From 479b8bab8a31cfca4ff284767604215524db6adb Mon Sep 17 00:00:00 2001 From: songyc macbook Date: Fri, 19 Jun 2026 19:53:54 +0900 Subject: [PATCH] restore project and add --- myApp/DeepWorkTimer | 1 - myApp/DeepWorkTimer/CLAUDE.md | 9 + .../DeepWorkTimer.xcodeproj/project.pbxproj | 526 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 15869 bytes .../xcschemes/xcschememanagement.plist | 19 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../DeepWorkTimer/ContentView.swift | 163 ++++++ .../DeepWorkTimer/DeepWorkTimer.entitlements | 8 + .../DeepWorkTimer/DeepWorkTimerApp.swift | 21 + .../DeepWorkTimer/Models/FocusSession.swift | 25 + .../Models/TimerAttributes.swift | 19 + .../ViewModels/TimerViewModel.swift | 133 +++++ .../DeepWorkTimer/Views/DashboardView.swift | 169 ++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../TimeWidget/Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + myApp/DeepWorkTimer/TimeWidget/Info.plist | 11 + .../DeepWorkTimer/TimeWidget/TimeWidget.swift | 84 +++ .../TimeWidget/TimeWidgetBundle.swift | 17 + .../TimeWidget/TimeWidgetLiveActivity.swift | 104 ++++ .../TimeWidget/TimerAttributes.swift | 19 + myApp/LemonLimeTracker | 1 - myApp/LemonLimeTracker/CLAUDE.md | 21 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../IOS/Assets.xcassets/Contents.json | 6 + myApp/LemonLimeTracker/IOS/ContentView.swift | 24 + myApp/LemonLimeTracker/IOS/Core/Theme.swift | 21 + .../IOS/Core/TimerActivityAttributes.swift | 14 + .../IOS/LemonLimeTrackerApp.swift | 19 + .../IOS/Models/Category.swift | 18 + myApp/LemonLimeTracker/IOS/Models/Goal.swift | 35 ++ .../IOS/Models/TaskItem.swift | 28 + .../LemonLimeTracker/IOS/Models/TaskLog.swift | 19 + .../IOS/Repositories/SwiftDataService.swift | 24 + .../Resources/en.lproj/Localizable.strings | 72 +++ .../Resources/ko.lproj/Localizable.strings | 72 +++ .../IOS/ViewModels/CategoryViewModel.swift | 45 ++ .../IOS/ViewModels/DashboardViewModel.swift | 84 +++ .../IOS/ViewModels/GoalViewModel.swift | 93 +++ .../IOS/ViewModels/SettingsViewModel.swift | 76 +++ .../IOS/ViewModels/TaskViewModel.swift | 157 +++++ .../IOS/Views/CategoryView.swift | 177 ++++++ .../IOS/Views/DashboardView.swift | 160 ++++++ .../LemonLimeTracker/IOS/Views/GoalView.swift | 306 ++++++++++ .../IOS/Views/LaunchScreenView.swift | 22 + .../IOS/Views/MainTabView.swift | 47 ++ .../LemonLimeTracker/IOS/Views/RootView.swift | 24 + .../IOS/Views/SettingsView.swift | 107 ++++ .../IOS/Views/TaskTrackerView.swift | 212 +++++++ .../LemonLimeTracker-Info.plist | 13 + .../project.pbxproj | 543 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 35877 bytes .../xcschemes/xcschememanagement.plist | 19 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + .../LemonLimeWidget/Info.plist | 11 + .../LemonLimeWidget/LemonLimeWidget.swift | 84 +++ .../LemonLimeWidgetBundle.swift | 17 + .../LemonLimeWidgetLiveActivity.swift | 125 ++++ 67 files changed, 4260 insertions(+), 2 deletions(-) delete mode 160000 myApp/DeepWorkTimer create mode 100644 myApp/DeepWorkTimer/CLAUDE.md create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.pbxproj create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/Contents.json create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/ContentView.swift create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimer.entitlements create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimerApp.swift create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/Models/FocusSession.swift create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/Models/TimerAttributes.swift create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/ViewModels/TimerViewModel.swift create mode 100644 myApp/DeepWorkTimer/DeepWorkTimer/Views/DashboardView.swift create mode 100644 myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/Contents.json create mode 100644 myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 myApp/DeepWorkTimer/TimeWidget/Info.plist create mode 100644 myApp/DeepWorkTimer/TimeWidget/TimeWidget.swift create mode 100644 myApp/DeepWorkTimer/TimeWidget/TimeWidgetBundle.swift create mode 100644 myApp/DeepWorkTimer/TimeWidget/TimeWidgetLiveActivity.swift create mode 100644 myApp/DeepWorkTimer/TimeWidget/TimerAttributes.swift delete mode 160000 myApp/LemonLimeTracker create mode 100644 myApp/LemonLimeTracker/CLAUDE.md create mode 100644 myApp/LemonLimeTracker/IOS/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 myApp/LemonLimeTracker/IOS/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 myApp/LemonLimeTracker/IOS/Assets.xcassets/Contents.json create mode 100644 myApp/LemonLimeTracker/IOS/ContentView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Core/Theme.swift create mode 100644 myApp/LemonLimeTracker/IOS/Core/TimerActivityAttributes.swift create mode 100644 myApp/LemonLimeTracker/IOS/LemonLimeTrackerApp.swift create mode 100644 myApp/LemonLimeTracker/IOS/Models/Category.swift create mode 100644 myApp/LemonLimeTracker/IOS/Models/Goal.swift create mode 100644 myApp/LemonLimeTracker/IOS/Models/TaskItem.swift create mode 100644 myApp/LemonLimeTracker/IOS/Models/TaskLog.swift create mode 100644 myApp/LemonLimeTracker/IOS/Repositories/SwiftDataService.swift create mode 100644 myApp/LemonLimeTracker/IOS/Resources/en.lproj/Localizable.strings create mode 100644 myApp/LemonLimeTracker/IOS/Resources/ko.lproj/Localizable.strings create mode 100644 myApp/LemonLimeTracker/IOS/ViewModels/CategoryViewModel.swift create mode 100644 myApp/LemonLimeTracker/IOS/ViewModels/DashboardViewModel.swift create mode 100644 myApp/LemonLimeTracker/IOS/ViewModels/GoalViewModel.swift create mode 100644 myApp/LemonLimeTracker/IOS/ViewModels/SettingsViewModel.swift create mode 100644 myApp/LemonLimeTracker/IOS/ViewModels/TaskViewModel.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/CategoryView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/DashboardView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/GoalView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/LaunchScreenView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/MainTabView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/RootView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/SettingsView.swift create mode 100644 myApp/LemonLimeTracker/IOS/Views/TaskTrackerView.swift create mode 100644 myApp/LemonLimeTracker/LemonLimeTracker-Info.plist create mode 100644 myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj create mode 100644 myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/Contents.json create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/Info.plist create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetBundle.swift create mode 100644 myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetLiveActivity.swift diff --git a/myApp/DeepWorkTimer b/myApp/DeepWorkTimer deleted file mode 160000 index 64afec6..0000000 --- a/myApp/DeepWorkTimer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 64afec696ed11456cd193859a3d2cc07abfc0097 diff --git a/myApp/DeepWorkTimer/CLAUDE.md b/myApp/DeepWorkTimer/CLAUDE.md new file mode 100644 index 0000000..8e1b8e7 --- /dev/null +++ b/myApp/DeepWorkTimer/CLAUDE.md @@ -0,0 +1,9 @@ +# Project: DeepWork Timer (iOS) +- Frameworks: SwiftUI, SwiftData, Swift Charts, ActivityKit +- Architecture: MVVM (Model-View-ViewModel) + +# Guidelines +- Only use native Apple frameworks. Do not use external APIs, Cocoapods, or third-party libraries. +- Never write the entire project at once. Follow the user's specific file/milestone targets. +- Explain iOS-specific concepts (e.g., @State, @Environment, MainActor) briefly when introducing them. +- Always implement safe SwiftData migrations and error handling. diff --git a/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.pbxproj b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.pbxproj new file mode 100644 index 0000000..37f6bdc --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.pbxproj @@ -0,0 +1,526 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 72106C082FDC71B400CDE600 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72106C072FDC71B400CDE600 /* WidgetKit.framework */; }; + 72106C0A2FDC71B400CDE600 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 72106C092FDC71B400CDE600 /* SwiftUI.framework */; }; + 72106C172FDC71B600CDE600 /* TimeWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 72106C152FDC71B600CDE600 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 72106BE22FDC6B3000CDE600 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 72106C042FDC71B400CDE600; + remoteInfo = TimeWidgetExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 72106C1C2FDC71B600CDE600 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 72106C172FDC71B600CDE600 /* TimeWidgetExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 72106BEA2FDC6B3000CDE600 /* DeepWorkTimer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DeepWorkTimer.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimeWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 72106C072FDC71B400CDE600 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 72106C092FDC71B400CDE600 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 72106C182FDC71B600CDE600 /* Exceptions for "TimeWidget" folder in "TimeWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 72106C042FDC71B400CDE600 /* TimeWidgetExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 72106BEC2FDC6B3000CDE600 /* DeepWorkTimer */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = DeepWorkTimer; + sourceTree = ""; + }; + 72106C0B2FDC71B400CDE600 /* TimeWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 72106C182FDC71B600CDE600 /* Exceptions for "TimeWidget" folder in "TimeWidgetExtension" target */, + ); + path = TimeWidget; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 72106BE72FDC6B3000CDE600 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72106C022FDC71B400CDE600 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 72106C0A2FDC71B400CDE600 /* SwiftUI.framework in Frameworks */, + 72106C082FDC71B400CDE600 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 72106BE12FDC6B3000CDE600 = { + isa = PBXGroup; + children = ( + 72106BEC2FDC6B3000CDE600 /* DeepWorkTimer */, + 72106C0B2FDC71B400CDE600 /* TimeWidget */, + 72106C062FDC71B400CDE600 /* Frameworks */, + 72106BEB2FDC6B3000CDE600 /* Products */, + ); + sourceTree = ""; + }; + 72106BEB2FDC6B3000CDE600 /* Products */ = { + isa = PBXGroup; + children = ( + 72106BEA2FDC6B3000CDE600 /* DeepWorkTimer.app */, + 72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 72106C062FDC71B400CDE600 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 72106C072FDC71B400CDE600 /* WidgetKit.framework */, + 72106C092FDC71B400CDE600 /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 72106BE92FDC6B3000CDE600 /* DeepWorkTimer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 72106BF52FDC6B3100CDE600 /* Build configuration list for PBXNativeTarget "DeepWorkTimer" */; + buildPhases = ( + 72106BE62FDC6B3000CDE600 /* Sources */, + 72106BE72FDC6B3000CDE600 /* Frameworks */, + 72106BE82FDC6B3000CDE600 /* Resources */, + 72106C1C2FDC71B600CDE600 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 72106C162FDC71B600CDE600 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 72106BEC2FDC6B3000CDE600 /* DeepWorkTimer */, + ); + name = DeepWorkTimer; + packageProductDependencies = ( + ); + productName = DeepWorkTimer; + productReference = 72106BEA2FDC6B3000CDE600 /* DeepWorkTimer.app */; + productType = "com.apple.product-type.application"; + }; + 72106C042FDC71B400CDE600 /* TimeWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 72106C192FDC71B600CDE600 /* Build configuration list for PBXNativeTarget "TimeWidgetExtension" */; + buildPhases = ( + 72106C012FDC71B400CDE600 /* Sources */, + 72106C022FDC71B400CDE600 /* Frameworks */, + 72106C032FDC71B400CDE600 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 72106C0B2FDC71B400CDE600 /* TimeWidget */, + ); + name = TimeWidgetExtension; + packageProductDependencies = ( + ); + productName = TimeWidgetExtension; + productReference = 72106C052FDC71B400CDE600 /* TimeWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 72106BE22FDC6B3000CDE600 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2650; + LastUpgradeCheck = 2650; + TargetAttributes = { + 72106BE92FDC6B3000CDE600 = { + CreatedOnToolsVersion = 26.5; + }; + 72106C042FDC71B400CDE600 = { + CreatedOnToolsVersion = 26.5; + }; + }; + }; + buildConfigurationList = 72106BE52FDC6B3000CDE600 /* Build configuration list for PBXProject "DeepWorkTimer" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 72106BE12FDC6B3000CDE600; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 72106BEB2FDC6B3000CDE600 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 72106BE92FDC6B3000CDE600 /* DeepWorkTimer */, + 72106C042FDC71B400CDE600 /* TimeWidgetExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 72106BE82FDC6B3000CDE600 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72106C032FDC71B400CDE600 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 72106BE62FDC6B3000CDE600 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 72106C012FDC71B400CDE600 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 72106C162FDC71B600CDE600 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 72106C042FDC71B400CDE600 /* TimeWidgetExtension */; + targetProxy = 72106C152FDC71B600CDE600 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 72106BF32FDC6B3100CDE600 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 72106BF42FDC6B3100CDE600 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 72106BF62FDC6B3100CDE600 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "DeepWorkTimer/DeepWorkTimer.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSSupportsLiveActivities = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 72106BF72FDC6B3100CDE600 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = "DeepWorkTimer/DeepWorkTimer.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSSupportsLiveActivities = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 72106C1A2FDC71B600CDE600 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TimeWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = TimeWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer.TimeWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 72106C1B2FDC71B600CDE600 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TimeWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = TimeWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.DeepWorkTimer.TimeWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 72106BE52FDC6B3000CDE600 /* Build configuration list for PBXProject "DeepWorkTimer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72106BF32FDC6B3100CDE600 /* Debug */, + 72106BF42FDC6B3100CDE600 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 72106BF52FDC6B3100CDE600 /* Build configuration list for PBXNativeTarget "DeepWorkTimer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72106BF62FDC6B3100CDE600 /* Debug */, + 72106BF72FDC6B3100CDE600 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 72106C192FDC71B600CDE600 /* Build configuration list for PBXNativeTarget "TimeWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 72106C1A2FDC71B600CDE600 /* Debug */, + 72106C1B2FDC71B600CDE600 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 72106BE22FDC6B3000CDE600 /* Project object */; +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..8553b76808c08a39cd66fc195833125cf3c8c5a9 GIT binary patch literal 15869 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 literal 0 HcmV?d00001 diff --git a/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..9055e13 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + DeepWorkTimer.xcscheme_^#shared#^_ + + orderHint + 1 + + TimeWidgetExtension.xcscheme_^#shared#^_ + + orderHint + 2 + + + + diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AccentColor.colorset/Contents.json b/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AppIcon.appiconset/Contents.json b/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/Contents.json b/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/ContentView.swift b/myApp/DeepWorkTimer/DeepWorkTimer/ContentView.swift new file mode 100644 index 0000000..25ccb2e --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/ContentView.swift @@ -0,0 +1,163 @@ +// +// ContentView.swift +// DeepWorkTimer +// +// Created by 송예찬 on 6/13/26. +// + +import SwiftUI +import SwiftData + +struct ContentView: View { + // @State owns the ViewModel; @Observable tracks only the properties this view reads. + @State private var viewModel = TimerViewModel() + @Query(sort: \FocusSession.startTime, order: .reverse) private var sessions: [FocusSession] + @Environment(\.modelContext) private var modelContext + // scenePhase tells us when the app moves to/from the background. + @Environment(\.scenePhase) private var scenePhase + + var body: some View { + TabView { + timerTab + .tabItem { + Label("Timer", systemImage: "timer") + } + + DashboardView() + .tabItem { + Label("Dashboard", systemImage: "chart.bar.fill") + } + } + .onChange(of: scenePhase) { _, newPhase in + switch newPhase { + case .background: + viewModel.appWentBackground() + case .active: + viewModel.appReturnedForeground() + default: + break + } + } + } + + private var timerTab: some View { + NavigationStack { + ScrollView { + VStack(spacing: 40) { + timerRing + controlButtons + if !sessions.isEmpty { + sessionList + } + } + .padding(.vertical, 24) + } + .navigationTitle("Deep Work Timer") + } + } + + // MARK: - Timer Ring + + private var timerRing: some View { + ZStack { + Circle() + .stroke(Color(.systemGray5), lineWidth: 18) + Circle() + .trim(from: 0, to: viewModel.progress) + .stroke( + Color.accentColor, + style: StrokeStyle(lineWidth: 18, lineCap: .round) + ) + .rotationEffect(.degrees(-90)) + .animation(.linear(duration: 1), value: viewModel.progress) + VStack(spacing: 6) { + Text(formattedTime) + .font(.system(size: 52, weight: .thin, design: .monospaced)) + Text(statusLabel) + .font(.caption) + .foregroundStyle(.secondary) + .textCase(.uppercase) + .tracking(2) + } + } + .frame(width: 270, height: 270) + .padding() + } + + private var formattedTime: String { + String(format: "%02d:%02d", viewModel.timeRemaining / 60, viewModel.timeRemaining % 60) + } + + private var statusLabel: String { + if viewModel.isRunning { return "Focus" } + if viewModel.sessionStartTime != nil { return "Paused" } + return "Ready" + } + + // MARK: - Control Buttons + + private var controlButtons: some View { + HStack(spacing: 20) { + Button { + viewModel.isRunning ? viewModel.pause() : viewModel.start() + } label: { + Label( + viewModel.isRunning ? "Pause" : "Start", + systemImage: viewModel.isRunning ? "pause.fill" : "play.fill" + ) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + + Button(role: .destructive) { + viewModel.stop(context: modelContext) + } label: { + Label("Stop", systemImage: "stop.fill") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .controlSize(.large) + .disabled(viewModel.sessionStartTime == nil) + } + .padding(.horizontal) + } + + // MARK: - Session List + + private var sessionList: some View { + VStack(alignment: .leading, spacing: 0) { + Text("Completed Sessions") + .font(.headline) + .padding(.horizontal) + .padding(.bottom, 12) + + ForEach(sessions) { session in + HStack { + VStack(alignment: .leading, spacing: 2) { + Text(session.category) + .font(.subheadline) + .fontWeight(.medium) + Text(session.startTime.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Text("\(session.durationInMinutes) min") + .font(.subheadline) + .foregroundStyle(.secondary) + } + .padding() + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.horizontal) + .padding(.vertical, 4) + } + } + } +} + +#Preview { + ContentView() + .modelContainer(for: FocusSession.self, inMemory: true) +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimer.entitlements b/myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimer.entitlements new file mode 100644 index 0000000..0ba9c39 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimer.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.live-activity + + + diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimerApp.swift b/myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimerApp.swift new file mode 100644 index 0000000..266cdf6 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/DeepWorkTimerApp.swift @@ -0,0 +1,21 @@ +// +// DeepWorkTimerApp.swift +// DeepWorkTimer +// +// Created by 송예찬 on 6/13/26. +// + +import SwiftUI +import SwiftData + +@main +struct DeepWorkTimerApp: App { + // modelContainer registers FocusSession with SwiftData and injects + // the store into the SwiftUI environment for all child views. + var body: some Scene { + WindowGroup { + ContentView() + } + .modelContainer(for: FocusSession.self) + } +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/Models/FocusSession.swift b/myApp/DeepWorkTimer/DeepWorkTimer/Models/FocusSession.swift new file mode 100644 index 0000000..efc80c3 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/Models/FocusSession.swift @@ -0,0 +1,25 @@ +// +// FocusSession.swift +// DeepWorkTimer +// +// Created by 송예찬 on 6/13/26. +// + +import Foundation +import SwiftData + +// @Model turns this plain Swift class into a SwiftData-managed, persistable entity. +@Model +final class FocusSession { + var id: UUID + var startTime: Date + var durationInMinutes: Int + var category: String + + init(id: UUID = UUID(), startTime: Date = .now, durationInMinutes: Int, category: String) { + self.id = id + self.startTime = startTime + self.durationInMinutes = durationInMinutes + self.category = category + } +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/Models/TimerAttributes.swift b/myApp/DeepWorkTimer/DeepWorkTimer/Models/TimerAttributes.swift new file mode 100644 index 0000000..dad149f --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/Models/TimerAttributes.swift @@ -0,0 +1,19 @@ +// Shared ActivityAttributes definition. +// NOTE: This file must belong to BOTH the DeepWorkTimer and TimeWidgetExtension targets. +// Because this project uses Xcode 16 file system synchronized groups, a physical copy +// lives in DeepWorkTimer/Models/ (for the app) and in TimeWidget/ (for the extension). +// Keep both files identical. + +import ActivityKit +import Foundation + +struct TimerAttributes: ActivityAttributes { + // ContentState holds data the system can update while the activity is live. + struct ContentState: Codable, Hashable { + // The exact moment the timer expires — drives Text(timerInterval:) real-time countdown. + var endDate: Date + } + + // Fixed for the lifetime of this activity. + var sessionName: String +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/ViewModels/TimerViewModel.swift b/myApp/DeepWorkTimer/DeepWorkTimer/ViewModels/TimerViewModel.swift new file mode 100644 index 0000000..15c8d28 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/ViewModels/TimerViewModel.swift @@ -0,0 +1,133 @@ +// +// TimerViewModel.swift +// DeepWorkTimer +// +// Created by 송예찬 on 6/13/26. +// + +import Foundation +import Observation +import SwiftData +import ActivityKit + +// @Observable replaces ObservableObject + @Published. +// SwiftUI views automatically track only the specific properties they read. +@Observable +final class TimerViewModel { + private(set) var timeRemaining: Int + private(set) var isRunning = false + private(set) var sessionStartTime: Date? + + let totalDuration: Int + + private var timerTask: Task? + // Stores the moment the app moved to the background so elapsed time + // can be calculated accurately when the app returns. + private var backgroundEntryDate: Date? + private var currentActivity: Activity? + + var progress: Double { + totalDuration > 0 ? Double(timeRemaining) / Double(totalDuration) : 1.0 + } + + init(duration: Int = 25 * 60) { + totalDuration = duration + timeRemaining = duration + } + + func start() { + guard !isRunning else { return } + if sessionStartTime == nil { + sessionStartTime = .now + } + isRunning = true + scheduleTick() + startLiveActivity() + } + + func pause() { + isRunning = false + cancelTick() + endLiveActivity() + } + + func stop(context: ModelContext) { + let start = sessionStartTime ?? .now + let elapsed = totalDuration - timeRemaining + let minutes = max(1, elapsed / 60) + context.insert(FocusSession(startTime: start, durationInMinutes: minutes, category: "Deep Work")) + endLiveActivity() + reset() + } + + // Call when scenePhase → .background + func appWentBackground() { + guard isRunning else { return } + backgroundEntryDate = .now + cancelTick() + // isRunning stays true so appReturnedForeground knows to resume. + } + + // Call when scenePhase → .active + func appReturnedForeground() { + guard isRunning, let entry = backgroundEntryDate else { return } + let elapsed = Int(Date.now.timeIntervalSince(entry)) + timeRemaining = max(0, timeRemaining - elapsed) + backgroundEntryDate = nil + if timeRemaining > 0 { + scheduleTick() + } else { + isRunning = false + endLiveActivity() + } + } + + private func startLiveActivity() { + guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } + let endDate = Date.now.addingTimeInterval(TimeInterval(timeRemaining)) + let state = TimerAttributes.ContentState(endDate: endDate) + let content = ActivityContent(state: state, staleDate: endDate) + let attributes = TimerAttributes(sessionName: "Deep Work") + do { + currentActivity = try Activity.request(attributes: attributes, content: content) + } catch { + // Live Activities may fail if the device doesn't support them or the limit is reached. + } + } + + private func endLiveActivity() { + guard let activity = currentActivity else { return } + currentActivity = nil + Task { + let finalState = TimerAttributes.ContentState(endDate: .now) + let content = ActivityContent(state: finalState, staleDate: nil) + await activity.end(content, dismissalPolicy: .immediate) + } + } + + private func scheduleTick() { + timerTask = Task { + while !Task.isCancelled && timeRemaining > 0 { + try? await Task.sleep(for: .seconds(1)) + guard !Task.isCancelled else { break } + timeRemaining = max(0, timeRemaining - 1) + } + if timeRemaining == 0 { + isRunning = false + endLiveActivity() + } + } + } + + private func cancelTick() { + timerTask?.cancel() + timerTask = nil + } + + private func reset() { + cancelTick() + isRunning = false + sessionStartTime = nil + timeRemaining = totalDuration + } +} diff --git a/myApp/DeepWorkTimer/DeepWorkTimer/Views/DashboardView.swift b/myApp/DeepWorkTimer/DeepWorkTimer/Views/DashboardView.swift new file mode 100644 index 0000000..5261534 --- /dev/null +++ b/myApp/DeepWorkTimer/DeepWorkTimer/Views/DashboardView.swift @@ -0,0 +1,169 @@ +// +// DashboardView.swift +// DeepWorkTimer +// +// Created by 송예찬 on 6/13/26. +// + +import SwiftUI +import SwiftData +import Charts + +// Lightweight struct to hold one day's aggregated focus data for the chart. +struct DayFocusData: Identifiable { + var id: Date { date } + let date: Date + let minutes: Int +} + +struct DashboardView: View { + // @Query fetches every persisted FocusSession from SwiftData automatically. + @Query(sort: \FocusSession.startTime, order: .forward) private var sessions: [FocusSession] + + // MARK: - Computed stats + + private var last7Days: [DayFocusData] { + let calendar = Calendar.current + let today = calendar.startOfDay(for: .now) + + var totals: [Date: Int] = [:] + for session in sessions { + let day = calendar.startOfDay(for: session.startTime) + totals[day, default: 0] += session.durationInMinutes + } + + return (0..<7).reversed().compactMap { offset -> DayFocusData? in + guard let day = calendar.date(byAdding: .day, value: -offset, to: today) else { return nil } + return DayFocusData(date: day, minutes: totals[day, default: 0]) + } + } + + private var totalFocusMinutes: Int { + sessions.reduce(0) { $0 + $1.durationInMinutes } + } + + private var averageDailyMinutes: Int { + let activeDays = last7Days.filter { $0.minutes > 0 } + guard !activeDays.isEmpty else { return 0 } + return activeDays.reduce(0) { $0 + $1.minutes } / activeDays.count + } + + // MARK: - Body + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + statsCards + chartSection + } + .padding(20) + } + .navigationTitle("Dashboard") + .background(Color(.systemGroupedBackground)) + } + } + + // MARK: - Stats Cards + + private var statsCards: some View { + HStack(spacing: 12) { + statCard( + title: "Total Focus Time", + value: formatted(totalFocusMinutes), + systemImage: "clock.fill", + tint: .blue + ) + statCard( + title: "Avg Daily Focus", + value: formatted(averageDailyMinutes), + systemImage: "chart.line.uptrend.xyaxis", + tint: .purple + ) + } + } + + private func statCard(title: String, value: String, systemImage: String, tint: Color) -> some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: systemImage) + .foregroundStyle(tint) + Spacer() + } + Text(value) + .font(.system(size: 32, weight: .bold, design: .rounded)) + .foregroundStyle(.primary) + .minimumScaleFactor(0.7) + .lineLimit(1) + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + } + .padding(16) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + + // MARK: - Chart Section + + private var chartSection: some View { + VStack(alignment: .leading, spacing: 14) { + Text("Last 7 Days") + .font(.headline) + .padding(.horizontal, 4) + + Chart(last7Days) { day in + BarMark( + x: .value("Day", day.date, unit: .day), + y: .value("Minutes", day.minutes) + ) + .foregroundStyle( + LinearGradient( + colors: [Color.blue.opacity(0.5), Color.purple], + startPoint: .bottom, + endPoint: .top + ) + ) + .cornerRadius(8) + } + .chartXAxis { + AxisMarks(values: .stride(by: .day)) { _ in + AxisValueLabel(format: .dateTime.weekday(.abbreviated)) + .font(.caption2) + } + } + .chartYAxis { + AxisMarks(position: .leading) { value in + AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4])) + .foregroundStyle(Color(.systemGray4)) + AxisValueLabel { + if let minutes = value.as(Int.self) { + Text("\(minutes)m") + .font(.caption2) + .foregroundStyle(.secondary) + } + } + } + } + .frame(height: 220) + .padding(16) + .background(Color(.secondarySystemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } + + // MARK: - Helpers + + private func formatted(_ totalMinutes: Int) -> String { + let hours = totalMinutes / 60 + let mins = totalMinutes % 60 + if hours > 0 { return "\(hours)h \(mins)m" } + return "\(mins)m" + } +} + +#Preview { + DashboardView() + .modelContainer(for: FocusSession.self, inMemory: true) +} diff --git a/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/Contents.json b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/DeepWorkTimer/TimeWidget/Info.plist b/myApp/DeepWorkTimer/TimeWidget/Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/myApp/DeepWorkTimer/TimeWidget/TimeWidget.swift b/myApp/DeepWorkTimer/TimeWidget/TimeWidget.swift new file mode 100644 index 0000000..2b2ced8 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/TimeWidget.swift @@ -0,0 +1,84 @@ +// +// TimeWidget.swift +// TimeWidget +// +// Created by 송예찬 on 6/13/26. +// + +import WidgetKit +import SwiftUI + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { + SimpleEntry(date: Date(), emoji: "😀") + } + + func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { + let entry = SimpleEntry(date: Date(), emoji: "😀") + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + var entries: [SimpleEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Date() + for hourOffset in 0 ..< 5 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! + let entry = SimpleEntry(date: entryDate, emoji: "😀") + entries.append(entry) + } + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } + +// func relevances() async -> WidgetRelevances { +// // Generate a list containing the contexts this widget is relevant in. +// } +} + +struct SimpleEntry: TimelineEntry { + let date: Date + let emoji: String +} + +struct TimeWidgetEntryView : View { + var entry: Provider.Entry + + var body: some View { + VStack { + Text("Time:") + Text(entry.date, style: .time) + + Text("Emoji:") + Text(entry.emoji) + } + } +} + +struct TimeWidget: Widget { + let kind: String = "TimeWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + TimeWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + TimeWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("My Widget") + .description("This is an example widget.") + } +} + +#Preview(as: .systemSmall) { + TimeWidget() +} timeline: { + SimpleEntry(date: .now, emoji: "😀") + SimpleEntry(date: .now, emoji: "🤩") +} diff --git a/myApp/DeepWorkTimer/TimeWidget/TimeWidgetBundle.swift b/myApp/DeepWorkTimer/TimeWidget/TimeWidgetBundle.swift new file mode 100644 index 0000000..3303ea7 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/TimeWidgetBundle.swift @@ -0,0 +1,17 @@ +// +// TimeWidgetBundle.swift +// TimeWidget +// +// Created by 송예찬 on 6/13/26. +// + +import WidgetKit +import SwiftUI + +@main +struct TimeWidgetBundle: WidgetBundle { + var body: some Widget { + TimeWidget() + TimeWidgetLiveActivity() + } +} diff --git a/myApp/DeepWorkTimer/TimeWidget/TimeWidgetLiveActivity.swift b/myApp/DeepWorkTimer/TimeWidget/TimeWidgetLiveActivity.swift new file mode 100644 index 0000000..12e0f61 --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/TimeWidgetLiveActivity.swift @@ -0,0 +1,104 @@ +// +// TimeWidgetLiveActivity.swift +// TimeWidget +// +// Created by 송예찬 on 6/13/26. +// + +import ActivityKit +import WidgetKit +import SwiftUI + +struct TimeWidgetLiveActivity: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: TimerAttributes.self) { context in + // Lock Screen / Notification Banner + // Text(timerInterval:) is a system primitive that counts down in real time + // without waking the app — no Timer or polling needed. + HStack(spacing: 14) { + Image(systemName: "timer") + .font(.title2.weight(.semibold)) + .foregroundStyle(.white) + + VStack(alignment: .leading, spacing: 2) { + Text(context.attributes.sessionName) + .font(.caption) + .foregroundStyle(.white.opacity(0.75)) + + let safeEnd = max(Date.now, context.state.endDate) + Text(timerInterval: Date.now...safeEnd, countsDown: true) + .font(.title.monospacedDigit().bold()) + .foregroundStyle(.white) + } + + Spacer() + } + .padding(.horizontal, 20) + .padding(.vertical, 14) + .activityBackgroundTint(Color(red: 0.07, green: 0.07, blue: 0.18)) + .activitySystemActionForegroundColor(.white) + + } dynamicIsland: { context in + DynamicIsland { + // Expanded — shown when the user long-presses the Dynamic Island. + DynamicIslandExpandedRegion(.leading) { + Label(context.attributes.sessionName, systemImage: "timer") + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + DynamicIslandExpandedRegion(.trailing) { + EmptyView() + } + DynamicIslandExpandedRegion(.center) { + let safeEnd = max(Date.now, context.state.endDate) + Text(timerInterval: Date.now...safeEnd, countsDown: true) + .font(.title2.monospacedDigit().bold()) + .foregroundStyle(.primary) + } + DynamicIslandExpandedRegion(.bottom) { + Text("Stay focused") + .font(.caption2) + .foregroundStyle(.tertiary) + } + } compactLeading: { + Image(systemName: "timer") + .foregroundStyle(.indigo) + } compactTrailing: { + let safeEnd = max(Date.now, context.state.endDate) + Text(timerInterval: Date.now...safeEnd, countsDown: true) + .font(.caption.monospacedDigit().bold()) + .foregroundStyle(.primary) + .frame(width: 52) + } minimal: { + Image(systemName: "timer") + .foregroundStyle(.indigo) + } + .keylineTint(.indigo) + } + } +} + +// MARK: - Previews + +extension TimerAttributes { + fileprivate static var preview: TimerAttributes { + TimerAttributes(sessionName: "Deep Work") + } +} + +extension TimerAttributes.ContentState { + fileprivate static var active: TimerAttributes.ContentState { + TimerAttributes.ContentState(endDate: Date.now.addingTimeInterval(22 * 60)) + } + fileprivate static var nearEnd: TimerAttributes.ContentState { + TimerAttributes.ContentState(endDate: Date.now.addingTimeInterval(2 * 60)) + } +} + +#Preview("Notification", as: .content, using: TimerAttributes.preview) { + TimeWidgetLiveActivity() +} contentStates: { + TimerAttributes.ContentState.active + TimerAttributes.ContentState.nearEnd +} diff --git a/myApp/DeepWorkTimer/TimeWidget/TimerAttributes.swift b/myApp/DeepWorkTimer/TimeWidget/TimerAttributes.swift new file mode 100644 index 0000000..dad149f --- /dev/null +++ b/myApp/DeepWorkTimer/TimeWidget/TimerAttributes.swift @@ -0,0 +1,19 @@ +// Shared ActivityAttributes definition. +// NOTE: This file must belong to BOTH the DeepWorkTimer and TimeWidgetExtension targets. +// Because this project uses Xcode 16 file system synchronized groups, a physical copy +// lives in DeepWorkTimer/Models/ (for the app) and in TimeWidget/ (for the extension). +// Keep both files identical. + +import ActivityKit +import Foundation + +struct TimerAttributes: ActivityAttributes { + // ContentState holds data the system can update while the activity is live. + struct ContentState: Codable, Hashable { + // The exact moment the timer expires — drives Text(timerInterval:) real-time countdown. + var endDate: Date + } + + // Fixed for the lifetime of this activity. + var sessionName: String +} diff --git a/myApp/LemonLimeTracker b/myApp/LemonLimeTracker deleted file mode 160000 index c66ea52..0000000 --- a/myApp/LemonLimeTracker +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c66ea52abf01a4199a6c0037c7211d13983f2c80 diff --git a/myApp/LemonLimeTracker/CLAUDE.md b/myApp/LemonLimeTracker/CLAUDE.md new file mode 100644 index 0000000..00a18fe --- /dev/null +++ b/myApp/LemonLimeTracker/CLAUDE.md @@ -0,0 +1,21 @@ +# LemonLimeTracker Project Guidelines + +## Core Rules & Constraints +- **NO Paid Entitlements**: NEVER add CloudKit, iCloud sync, App Groups, or Remote Notifications to Xcode Signing & Capabilities. Avoid build errors. +- **Persistence**: Use strictly local SwiftData. Do NOT use `NSPersistentCloudKitContainer`. + +## Architecture & Future Scope +- **Pattern**: Clean Architecture + MVVM. +- **Repository Pattern**: Strict separation of Data processing (SwiftData) and UI Views to allow easy future integration of CloudKit, Apple Watch, and Widgets. +- **Folder Structure**: + - `IOS/Models`: SwiftData Models (Task, Category, Goal, Log). + - `IOS/Repositories`: Data access layer (Protocols + Implementations). + - `IOS/ViewModels`: Business logic (`@Observable`). + - `IOS/Views`: SwiftUI Views. + - `IOS/Core`: App entry point, Constants, ActivityKit. + - `IOS/Resources`: Assets, Info.plist, Localizable.strings. + +## UI / Design System +- **Light Mode**: Green, Yellow, White. +- **Dark Mode**: Green, Yellow, Black. +- **Localization**: Default is Korean. Support English. diff --git a/myApp/LemonLimeTracker/IOS/Assets.xcassets/AccentColor.colorset/Contents.json b/myApp/LemonLimeTracker/IOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/IOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/myApp/LemonLimeTracker/IOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/IOS/Assets.xcassets/Contents.json b/myApp/LemonLimeTracker/IOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/IOS/ContentView.swift b/myApp/LemonLimeTracker/IOS/ContentView.swift new file mode 100644 index 0000000..e245bef --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// LemonLimeTracker +// +// Created by 송예찬 on 6/18/26. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/myApp/LemonLimeTracker/IOS/Core/Theme.swift b/myApp/LemonLimeTracker/IOS/Core/Theme.swift new file mode 100644 index 0000000..a62070a --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Core/Theme.swift @@ -0,0 +1,21 @@ +import SwiftUI +import UIKit + +enum Theme { + static let green = Color(red: 0.18, green: 0.72, blue: 0.32) + static let yellow = Color(red: 0.96, green: 0.86, blue: 0.12) + + static let background = Color(UIColor { traits in + traits.userInterfaceStyle == .dark ? .black : .white + }) + + static let surface = Color(UIColor { traits in + traits.userInterfaceStyle == .dark + ? UIColor(red: 0.10, green: 0.10, blue: 0.10, alpha: 1) + : UIColor(red: 0.97, green: 0.97, blue: 0.97, alpha: 1) + }) + + static let primaryText = Color(UIColor { traits in + traits.userInterfaceStyle == .dark ? .white : .black + }) +} diff --git a/myApp/LemonLimeTracker/IOS/Core/TimerActivityAttributes.swift b/myApp/LemonLimeTracker/IOS/Core/TimerActivityAttributes.swift new file mode 100644 index 0000000..4b3f522 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Core/TimerActivityAttributes.swift @@ -0,0 +1,14 @@ +#if os(iOS) +import ActivityKit +import Foundation + +struct TimerActivityAttributes: ActivityAttributes { + struct ContentState: Codable, Hashable { + var startDate: Date + var elapsedSeconds: Int + } + + var taskName: String + var taskIcon: String +} +#endif diff --git a/myApp/LemonLimeTracker/IOS/LemonLimeTrackerApp.swift b/myApp/LemonLimeTracker/IOS/LemonLimeTrackerApp.swift new file mode 100644 index 0000000..434cfec --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/LemonLimeTrackerApp.swift @@ -0,0 +1,19 @@ +// +// LemonLimeTrackerApp.swift +// LemonLimeTracker +// +// Created by 송예찬 on 6/18/26. +// + +import SwiftUI +import SwiftData + +@main +struct LemonLimeTrackerApp: App { + var body: some Scene { + WindowGroup { + RootView() + } + .modelContainer(SwiftDataService.shared.container) + } +} diff --git a/myApp/LemonLimeTracker/IOS/Models/Category.swift b/myApp/LemonLimeTracker/IOS/Models/Category.swift new file mode 100644 index 0000000..79339cb --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Models/Category.swift @@ -0,0 +1,18 @@ +import Foundation +import SwiftData + +@Model +final class Category { + @Attribute(.unique) var id: UUID + var name: String + var colorHex: String + + @Relationship(deleteRule: .cascade, inverse: \TaskItem.category) + var tasks: [TaskItem] = [] + + init(id: UUID = UUID(), name: String, colorHex: String) { + self.id = id + self.name = name + self.colorHex = colorHex + } +} diff --git a/myApp/LemonLimeTracker/IOS/Models/Goal.swift b/myApp/LemonLimeTracker/IOS/Models/Goal.swift new file mode 100644 index 0000000..959e939 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Models/Goal.swift @@ -0,0 +1,35 @@ +import Foundation +import SwiftData + +enum GoalFrequency: String, Codable { + case daily + case weekly + case monthly +} + +@Model +final class Goal { + @Attribute(.unique) var id: UUID + var targetType: String // "count" or "duration" + var conditions: Double // threshold value to meet the goal + var frequency: GoalFrequency + + var task: TaskItem? + var category: Category? + + init( + id: UUID = UUID(), + targetType: String, + conditions: Double, + frequency: GoalFrequency, + task: TaskItem? = nil, + category: Category? = nil + ) { + self.id = id + self.targetType = targetType + self.conditions = conditions + self.frequency = frequency + self.task = task + self.category = category + } +} diff --git a/myApp/LemonLimeTracker/IOS/Models/TaskItem.swift b/myApp/LemonLimeTracker/IOS/Models/TaskItem.swift new file mode 100644 index 0000000..bbe039e --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Models/TaskItem.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftData + +enum TaskType: String, Codable { + case count + case time +} + +@Model +final class TaskItem { + @Attribute(.unique) var id: UUID + var name: String + var icon: String + var type: TaskType + + var category: Category? + + @Relationship(deleteRule: .cascade, inverse: \TaskLog.task) + var logs: [TaskLog] = [] + + init(id: UUID = UUID(), name: String, icon: String, type: TaskType, category: Category? = nil) { + self.id = id + self.name = name + self.icon = icon + self.type = type + self.category = category + } +} diff --git a/myApp/LemonLimeTracker/IOS/Models/TaskLog.swift b/myApp/LemonLimeTracker/IOS/Models/TaskLog.swift new file mode 100644 index 0000000..ccd5273 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Models/TaskLog.swift @@ -0,0 +1,19 @@ +import Foundation +import SwiftData + +@Model +final class TaskLog { + @Attribute(.unique) var id: UUID + var date: Date + var duration: TimeInterval + var count: Int + + var task: TaskItem? + + init(id: UUID = UUID(), date: Date = .now, duration: TimeInterval = 0, count: Int = 0) { + self.id = id + self.date = date + self.duration = duration + self.count = count + } +} diff --git a/myApp/LemonLimeTracker/IOS/Repositories/SwiftDataService.swift b/myApp/LemonLimeTracker/IOS/Repositories/SwiftDataService.swift new file mode 100644 index 0000000..b056f1e --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Repositories/SwiftDataService.swift @@ -0,0 +1,24 @@ +import Foundation +import SwiftData + +@MainActor +final class SwiftDataService { + static let shared = SwiftDataService() + + let container: ModelContainer + + private init() { + let schema = Schema([ + Category.self, + TaskItem.self, + TaskLog.self, + Goal.self + ]) + let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) + do { + container = try ModelContainer(for: schema, configurations: config) + } catch { + fatalError("SwiftData container failed to initialize: \(error)") + } + } +} diff --git a/myApp/LemonLimeTracker/IOS/Resources/en.lproj/Localizable.strings b/myApp/LemonLimeTracker/IOS/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..0d88802 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Resources/en.lproj/Localizable.strings @@ -0,0 +1,72 @@ +/* Tab Bar */ +"tab.dashboard" = "Dashboard"; +"tab.category" = "Category"; +"tab.tasks" = "Tasks"; +"tab.goals" = "Goals"; +"tab.settings" = "Settings"; + +/* Task Tracker */ +"task.empty" = "No tasks added yet"; +"task.add" = "Add Task"; +"task.name" = "Name"; +"task.icon" = "Icon"; +"task.type" = "Type"; +"task.type.count" = "Count"; +"task.type.time" = "Time"; +"task.add.count" = "Add Count"; +"task.delete" = "Delete"; +"task.category" = "Category"; +"task.category.none" = "None"; + +/* Buttons */ +"button.cancel" = "Cancel"; +"button.add" = "Add"; + +/* Category */ +"category.empty" = "No categories yet"; +"category.add" = "Add Category"; +"category.name" = "Name"; +"category.color" = "Color"; +"category.delete" = "Delete"; +"category.task.count" = "tasks"; + +/* Goals */ +"goal.empty" = "No goals yet"; +"goal.add" = "Add Goal"; +"goal.chart.title" = "Goal Progress"; +"goal.progress" = "Progress"; +"goal.target" = "Target"; +"goal.target.task" = "Task"; +"goal.target.category" = "Category"; +"goal.no.target" = "None"; +"goal.condition" = "Condition"; +"goal.condition.count" = "Count"; +"goal.condition.time" = "Time"; +"goal.frequency" = "Frequency"; +"goal.frequency.daily" = "Daily"; +"goal.frequency.weekly" = "Weekly"; +"goal.frequency.monthly" = "Monthly"; +"goal.threshold" = "Target Value"; +"goal.delete" = "Delete"; + +/* Dashboard */ +"dashboard.progress.title" = "Today's Progress"; +"dashboard.goals.title" = "Top Goals"; +"dashboard.tasks.title" = "Remaining Tasks"; +"dashboard.tasks.empty" = "All tasks done for today!"; + +/* Settings */ +"settings.section.general" = "Preferences"; +"settings.language" = "Language"; +"settings.language.system" = "System"; +"settings.firstWeekday" = "Start of Week"; +"settings.firstWeekday.sunday" = "Sunday"; +"settings.firstWeekday.monday" = "Monday"; +"settings.dayStart" = "Start of Day"; +"settings.section.premium" = "Premium Features"; +"settings.premium.footnote" = "Premium Feature — coming soon"; +"settings.premium.cloudSync" = "Cloud Data Sync"; +"settings.premium.appleWatch" = "Apple Watch Support"; +"settings.premium.ipadMacSync" = "iPad & Mac Sync"; +"settings.premium.widgets" = "Home/Lock Screen Widgets"; +"settings.premium.siriShortcuts" = "Siri & Shortcuts"; diff --git a/myApp/LemonLimeTracker/IOS/Resources/ko.lproj/Localizable.strings b/myApp/LemonLimeTracker/IOS/Resources/ko.lproj/Localizable.strings new file mode 100644 index 0000000..6e2b31d --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Resources/ko.lproj/Localizable.strings @@ -0,0 +1,72 @@ +/* Tab Bar */ +"tab.dashboard" = "대시보드"; +"tab.category" = "카테고리"; +"tab.tasks" = "작업 추적"; +"tab.goals" = "목표"; +"tab.settings" = "설정"; + +/* Task Tracker */ +"task.empty" = "등록된 작업이 없습니다"; +"task.add" = "작업 추가"; +"task.name" = "이름"; +"task.icon" = "아이콘"; +"task.type" = "유형"; +"task.type.count" = "횟수"; +"task.type.time" = "시간"; +"task.add.count" = "횟수 추가"; +"task.delete" = "삭제"; +"task.category" = "카테고리"; +"task.category.none" = "없음"; + +/* Buttons */ +"button.cancel" = "취소"; +"button.add" = "추가"; + +/* Category */ +"category.empty" = "카테고리가 없습니다"; +"category.add" = "카테고리 추가"; +"category.name" = "이름"; +"category.color" = "색상"; +"category.delete" = "삭제"; +"category.task.count" = "작업"; + +/* Goals */ +"goal.empty" = "등록된 목표가 없습니다"; +"goal.add" = "목표 추가"; +"goal.chart.title" = "목표 진행률"; +"goal.progress" = "진행률"; +"goal.target" = "대상"; +"goal.target.task" = "작업"; +"goal.target.category" = "카테고리"; +"goal.no.target" = "없음"; +"goal.condition" = "조건"; +"goal.condition.count" = "횟수"; +"goal.condition.time" = "시간"; +"goal.frequency" = "주기"; +"goal.frequency.daily" = "매일"; +"goal.frequency.weekly" = "매주"; +"goal.frequency.monthly" = "매월"; +"goal.threshold" = "목표값"; +"goal.delete" = "삭제"; + +/* Dashboard */ +"dashboard.progress.title" = "오늘의 진행률"; +"dashboard.goals.title" = "주요 목표"; +"dashboard.tasks.title" = "남은 작업"; +"dashboard.tasks.empty" = "오늘의 작업을 모두 완료했습니다!"; + +/* Settings */ +"settings.section.general" = "환경설정"; +"settings.language" = "언어"; +"settings.language.system" = "시스템"; +"settings.firstWeekday" = "주 시작일"; +"settings.firstWeekday.sunday" = "일요일"; +"settings.firstWeekday.monday" = "월요일"; +"settings.dayStart" = "하루 시작 시간"; +"settings.section.premium" = "프리미엄 기능"; +"settings.premium.footnote" = "프리미엄 기능 — 출시 예정"; +"settings.premium.cloudSync" = "클라우드 데이터 동기화"; +"settings.premium.appleWatch" = "Apple Watch 지원"; +"settings.premium.ipadMacSync" = "iPad 및 Mac 동기화"; +"settings.premium.widgets" = "홈/잠금 화면 위젯"; +"settings.premium.siriShortcuts" = "Siri 및 단축어"; diff --git a/myApp/LemonLimeTracker/IOS/ViewModels/CategoryViewModel.swift b/myApp/LemonLimeTracker/IOS/ViewModels/CategoryViewModel.swift new file mode 100644 index 0000000..a7a858b --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/ViewModels/CategoryViewModel.swift @@ -0,0 +1,45 @@ +import Foundation +import Observation +import SwiftData + +@MainActor +@Observable +final class CategoryViewModel { + private(set) var categories: [Category] = [] + private var context: ModelContext? + + static let presetColors: [String] = [ + "#2EB852", "#F5DC1F", "#4A90D9", "#F5A623", + "#9B59B6", "#E74C3C", "#1ABC9C", "#F39C12" + ] + + func setup(context: ModelContext) { + self.context = context + fetchCategories() + } + + func fetchCategories() { + guard let context else { return } + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.name)]) + categories = (try? context.fetch(descriptor)) ?? [] + } + + func addCategory(name: String, colorHex: String) { + guard let context else { return } + let category = Category(name: name, colorHex: colorHex) + context.insert(category) + try? context.save() + fetchCategories() + } + + func deleteCategory(_ category: Category) { + guard let context else { return } + context.delete(category) + try? context.save() + fetchCategories() + } + + func taskCount(for category: Category) -> Int { + category.tasks.count + } +} diff --git a/myApp/LemonLimeTracker/IOS/ViewModels/DashboardViewModel.swift b/myApp/LemonLimeTracker/IOS/ViewModels/DashboardViewModel.swift new file mode 100644 index 0000000..85a12a5 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/ViewModels/DashboardViewModel.swift @@ -0,0 +1,84 @@ +import Foundation +import Observation +import SwiftData + +@MainActor +@Observable +final class DashboardViewModel { + private(set) var dailyGoalProgress: [GoalProgress] = [] + private(set) var remainingTasks: [TaskItem] = [] + private(set) var completedTaskCount: Int = 0 + private(set) var totalTaskCount: Int = 0 + + private var context: ModelContext? + + var overallProgress: Double { + if !dailyGoalProgress.isEmpty { + let sum = dailyGoalProgress.reduce(0.0) { $0 + $1.ratio } + return sum / Double(dailyGoalProgress.count) + } + guard totalTaskCount > 0 else { return 0 } + return Double(completedTaskCount) / Double(totalTaskCount) + } + + var topGoalProgress: [GoalProgress] { + Array(dailyGoalProgress.sorted { $0.ratio < $1.ratio }.prefix(3)) + } + + func setup(context: ModelContext) { + self.context = context + refresh() + } + + func refresh() { + guard let context else { return } + + let goalDescriptor = FetchDescriptor() + let goals = (try? context.fetch(goalDescriptor)) ?? [] + let dailyGoals = goals.filter { $0.frequency == .daily } + dailyGoalProgress = dailyGoals.map { goal in + let current = todayValue(for: goal) + return GoalProgress(goal: goal, current: current, target: goal.conditions) + } + + let taskDescriptor = FetchDescriptor(sortBy: [SortDescriptor(\.name)]) + let tasks = (try? context.fetch(taskDescriptor)) ?? [] + totalTaskCount = tasks.count + remainingTasks = tasks.filter { !isCompletedToday($0) } + completedTaskCount = totalTaskCount - remainingTasks.count + } + + func addCount(to task: TaskItem) { + guard let context else { return } + let log = TaskLog(date: .now, duration: 0, count: 1) + log.task = task + context.insert(log) + try? context.save() + refresh() + } + + private func isCompletedToday(_ task: TaskItem) -> Bool { + let todayLogs = task.logs.filter { Calendar.current.isDateInToday($0.date) } + switch task.type { + case .count: + return todayLogs.reduce(0) { $0 + $1.count } > 0 + case .time: + return !todayLogs.isEmpty + } + } + + private func todayValue(for goal: Goal) -> Double { + let logs: [TaskLog] + if let task = goal.task { + logs = task.logs + } else if let category = goal.category { + logs = category.tasks.flatMap { $0.logs } + } else { + logs = [] + } + let todayLogs = logs.filter { Calendar.current.isDateInToday($0.date) } + return goal.targetType == "count" + ? Double(todayLogs.reduce(0) { $0 + $1.count }) + : todayLogs.reduce(0.0) { $0 + $1.duration } + } +} diff --git a/myApp/LemonLimeTracker/IOS/ViewModels/GoalViewModel.swift b/myApp/LemonLimeTracker/IOS/ViewModels/GoalViewModel.swift new file mode 100644 index 0000000..196b392 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/ViewModels/GoalViewModel.swift @@ -0,0 +1,93 @@ +import Foundation +import Observation +import SwiftData + +enum GoalTargetType: String, CaseIterable { + case task + case category +} + +struct GoalProgress: Identifiable { + var id: UUID { goal.id } + let goal: Goal + let current: Double + let target: Double + + var ratio: Double { target > 0 ? min(current / target, 1.0) : 0.0 } + var isCompleted: Bool { current >= target } + + var displayName: String { + if let task = goal.task { return "\(task.icon) \(task.name)" } + if let category = goal.category { return category.name } + return "—" + } + + var percentText: String { "\(Int(ratio * 100))%" } +} + +@MainActor +@Observable +final class GoalViewModel { + private(set) var goals: [Goal] = [] + private(set) var goalProgress: [GoalProgress] = [] + private var context: ModelContext? + + func setup(context: ModelContext) { + self.context = context + fetchGoals() + } + + func fetchGoals() { + guard let context else { return } + let descriptor = FetchDescriptor() + goals = (try? context.fetch(descriptor)) ?? [] + computeProgress() + } + + func addGoal(targetType: String, conditions: Double, frequency: GoalFrequency, task: TaskItem?, category: Category?) { + guard let context else { return } + let goal = Goal(targetType: targetType, conditions: conditions, frequency: frequency, task: task, category: category) + context.insert(goal) + try? context.save() + fetchGoals() + } + + func deleteGoal(_ goal: Goal) { + guard let context else { return } + context.delete(goal) + try? context.save() + fetchGoals() + } + + private func computeProgress() { + let now = Date.now + let cal = Calendar.current + goalProgress = goals.map { goal in + let logs = relevantLogs(for: goal, reference: now, calendar: cal) + let current: Double = goal.targetType == "count" + ? Double(logs.reduce(0) { $0 + $1.count }) + : logs.reduce(0.0) { $0 + $1.duration } + return GoalProgress(goal: goal, current: current, target: goal.conditions) + } + } + + private func relevantLogs(for goal: Goal, reference: Date, calendar: Calendar) -> [TaskLog] { + let allLogs: [TaskLog] + if let task = goal.task { + allLogs = task.logs + } else if let category = goal.category { + allLogs = category.tasks.flatMap { $0.logs } + } else { + return [] + } + return allLogs.filter { inPeriod($0.date, frequency: goal.frequency, reference: reference, calendar: calendar) } + } + + private func inPeriod(_ date: Date, frequency: GoalFrequency, reference: Date, calendar: Calendar) -> Bool { + switch frequency { + case .daily: return calendar.isDate(date, inSameDayAs: reference) + case .weekly: return calendar.isDate(date, equalTo: reference, toGranularity: .weekOfYear) + case .monthly: return calendar.isDate(date, equalTo: reference, toGranularity: .month) + } + } +} diff --git a/myApp/LemonLimeTracker/IOS/ViewModels/SettingsViewModel.swift b/myApp/LemonLimeTracker/IOS/ViewModels/SettingsViewModel.swift new file mode 100644 index 0000000..cb5ec61 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/ViewModels/SettingsViewModel.swift @@ -0,0 +1,76 @@ +import Foundation +import Observation + +enum AppLanguage: String, CaseIterable, Identifiable { + case system + case english + case korean + + var id: String { rawValue } + + var displayName: String { + switch self { + case .system: return String(localized: "settings.language.system") + case .english: return "English" + case .korean: return "한국어" + } + } +} + +enum FirstWeekday: Int, CaseIterable, Identifiable { + case sunday = 1 + case monday = 2 + + var id: Int { rawValue } + + var displayName: String { + switch self { + case .sunday: return String(localized: "settings.firstWeekday.sunday") + case .monday: return String(localized: "settings.firstWeekday.monday") + } + } +} + +@MainActor +@Observable +final class SettingsViewModel { + private static let languageKey = "settings.language" + private static let firstWeekdayKey = "settings.firstWeekday" + private static let dayStartMinutesKey = "settings.dayStartMinutes" + + static let defaultDayStartMinutes = 4 * 60 // 04:00 AM + + var language: AppLanguage { + didSet { UserDefaults.standard.set(language.rawValue, forKey: Self.languageKey) } + } + + var firstWeekday: FirstWeekday { + didSet { UserDefaults.standard.set(firstWeekday.rawValue, forKey: Self.firstWeekdayKey) } + } + + /// Minutes since midnight marking when a "day" starts for goal/log purposes. + var dayStartMinutes: Int { + didSet { UserDefaults.standard.set(dayStartMinutes, forKey: Self.dayStartMinutesKey) } + } + + var dayStartDate: Date { + get { + Calendar.current.date(bySettingHour: dayStartMinutes / 60, minute: dayStartMinutes % 60, second: 0, of: .now) ?? .now + } + set { + let comps = Calendar.current.dateComponents([.hour, .minute], from: newValue) + dayStartMinutes = (comps.hour ?? 0) * 60 + (comps.minute ?? 0) + } + } + + init() { + let defaults = UserDefaults.standard + language = AppLanguage(rawValue: defaults.string(forKey: Self.languageKey) ?? "") ?? .system + firstWeekday = FirstWeekday(rawValue: defaults.integer(forKey: Self.firstWeekdayKey)) ?? .sunday + if let storedMinutes = defaults.object(forKey: Self.dayStartMinutesKey) as? Int { + dayStartMinutes = storedMinutes + } else { + dayStartMinutes = Self.defaultDayStartMinutes + } + } +} diff --git a/myApp/LemonLimeTracker/IOS/ViewModels/TaskViewModel.swift b/myApp/LemonLimeTracker/IOS/ViewModels/TaskViewModel.swift new file mode 100644 index 0000000..0923425 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/ViewModels/TaskViewModel.swift @@ -0,0 +1,157 @@ +import Foundation +import Observation +import SwiftData +#if os(iOS) +import ActivityKit +#endif + +@MainActor +@Observable +final class TaskViewModel { + private(set) var tasks: [TaskItem] = [] + private(set) var activeTimerStartDates: [UUID: Date] = [:] + private(set) var elapsedSeconds: [UUID: Int] = [:] + + private var context: ModelContext? + private var timerTasks: [UUID: Task] = [:] +#if os(iOS) + private var liveActivities: [UUID: Activity] = [:] +#endif + + func setup(context: ModelContext) { + self.context = context + fetchTasks() + } + + func fetchTasks() { + guard let context else { return } + let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\.name)]) + tasks = (try? context.fetch(descriptor)) ?? [] + } + + func addTask(name: String, icon: String, type: TaskType, category: Category? = nil) { + guard let context else { return } + let item = TaskItem(name: name, icon: icon, type: type, category: category) + context.insert(item) + try? context.save() + fetchTasks() + } + + func deleteTask(_ task: TaskItem) { + stopTimer(for: task) + guard let context else { return } + context.delete(task) + try? context.save() + fetchTasks() + } + + func addCount(to task: TaskItem) { + guard let context else { return } + let log = TaskLog(date: .now, duration: 0, count: 1) + log.task = task + context.insert(log) + try? context.save() + } + + func toggleTimer(for task: TaskItem) { + if activeTimerStartDates[task.id] != nil { + stopTimer(for: task) + } else { + startTimer(for: task) + } + } + + func isTimerRunning(for taskID: UUID) -> Bool { + activeTimerStartDates[taskID] != nil + } + + func formattedElapsed(for taskID: UUID) -> String { + let s = elapsedSeconds[taskID] ?? 0 + let h = s / 3600 + let m = (s % 3600) / 60 + let sec = s % 60 + return h > 0 + ? String(format: "%d:%02d:%02d", h, m, sec) + : String(format: "%02d:%02d", m, sec) + } + + func todayCount(for task: TaskItem) -> Int { + task.logs + .filter { Calendar.current.isDateInToday($0.date) } + .reduce(0) { $0 + $1.count } + } + + // MARK: - Timer + + private func startTimer(for task: TaskItem) { + let startDate = Date.now + activeTimerStartDates[task.id] = startDate + elapsedSeconds[task.id] = 0 + startLiveActivity(for: task, startDate: startDate) + + let taskID = task.id + timerTasks[taskID] = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep(for: .seconds(1)) + guard !Task.isCancelled, let self else { break } + let elapsed = Int(Date.now.timeIntervalSince(startDate)) + self.elapsedSeconds[taskID] = elapsed + await self.updateLiveActivity(taskID: taskID, elapsed: elapsed, startDate: startDate) + } + } + } + + private func stopTimer(for task: TaskItem) { + guard let startDate = activeTimerStartDates[task.id] else { return } + + timerTasks[task.id]?.cancel() + timerTasks.removeValue(forKey: task.id) + + let duration = Date.now.timeIntervalSince(startDate) + activeTimerStartDates.removeValue(forKey: task.id) + elapsedSeconds.removeValue(forKey: task.id) + + endLiveActivity(for: task) + + guard let context else { return } + let log = TaskLog(date: .now, duration: duration, count: 0) + log.task = task + context.insert(log) + try? context.save() + } + + // MARK: - ActivityKit + + private func startLiveActivity(for task: TaskItem, startDate: Date) { +#if os(iOS) + guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } + let attrs = TimerActivityAttributes(taskName: task.name, taskIcon: task.icon) + let state = TimerActivityAttributes.ContentState(startDate: startDate, elapsedSeconds: 0) + let content = ActivityContent(state: state, staleDate: nil) + do { + let activity = try Activity.request(attributes: attrs, content: content) + liveActivities[task.id] = activity + } catch { + // Live Activity unavailable (simulator, denied, or OS < 16.2) + } +#endif + } + + private func updateLiveActivity(taskID: UUID, elapsed: Int, startDate: Date) async { +#if os(iOS) + guard let activity = liveActivities[taskID] else { return } + let state = TimerActivityAttributes.ContentState(startDate: startDate, elapsedSeconds: elapsed) + await activity.update(ActivityContent(state: state, staleDate: nil)) +#endif + } + + private func endLiveActivity(for task: TaskItem) { +#if os(iOS) + guard let activity = liveActivities[task.id] else { return } + liveActivities.removeValue(forKey: task.id) + Task { + await activity.end(nil, dismissalPolicy: .immediate) + } +#endif + } +} diff --git a/myApp/LemonLimeTracker/IOS/Views/CategoryView.swift b/myApp/LemonLimeTracker/IOS/Views/CategoryView.swift new file mode 100644 index 0000000..2b6369f --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/CategoryView.swift @@ -0,0 +1,177 @@ +import SwiftUI +import SwiftData + +struct CategoryView: View { + @Environment(\.modelContext) private var modelContext + @State private var viewModel = CategoryViewModel() + @State private var showAddSheet = false + + var body: some View { + ZStack { + Theme.background.ignoresSafeArea() + Group { + if viewModel.categories.isEmpty { + emptyState + } else { + categoryList + } + } + } + .navigationTitle(String(localized: "tab.category")) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { showAddSheet = true } label: { + Image(systemName: "plus") + .foregroundStyle(Theme.green) + } + } + } + .sheet(isPresented: $showAddSheet) { + AddCategorySheet { name, colorHex in + viewModel.addCategory(name: name, colorHex: colorHex) + } + } + .onAppear { viewModel.setup(context: modelContext) } + } + + private var emptyState: some View { + VStack(spacing: 16) { + Image(systemName: "folder.badge.plus") + .font(.system(size: 56)) + .foregroundStyle(Theme.green.opacity(0.6)) + Text(String(localized: "category.empty")) + .foregroundStyle(Theme.primaryText.opacity(0.6)) + } + } + + private var categoryList: some View { + List { + ForEach(viewModel.categories) { category in + CategoryRow(category: category, taskCount: viewModel.taskCount(for: category)) + .listRowBackground(Theme.surface) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + viewModel.deleteCategory(category) + } label: { + Label(String(localized: "category.delete"), systemImage: "trash") + } + } + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } +} + +// MARK: - Category Row + +private struct CategoryRow: View { + let category: Category + let taskCount: Int + + var body: some View { + HStack(spacing: 12) { + Circle() + .fill(Color(hex: category.colorHex)) + .frame(width: 14, height: 14) + + Text(category.name) + .font(.body) + .fontWeight(.medium) + .foregroundStyle(Theme.primaryText) + + Spacer() + + Text("\(taskCount) \(String(localized: "category.task.count"))") + .font(.caption) + .foregroundStyle(Theme.primaryText.opacity(0.5)) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Theme.green.opacity(0.12), in: Capsule()) + } + .padding(.vertical, 6) + } +} + +// MARK: - Add Category Sheet + +private struct AddCategorySheet: View { + @Environment(\.dismiss) private var dismiss + @State private var name = "" + @State private var selectedColor = CategoryViewModel.presetColors[0] + + let onAdd: (String, String) -> Void + + var body: some View { + NavigationStack { + Form { + Section { + TextField(String(localized: "category.name"), text: $name) + } + + Section(String(localized: "category.color")) { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 4), spacing: 16) { + ForEach(CategoryViewModel.presetColors, id: \.self) { colorHex in + Circle() + .fill(Color(hex: colorHex)) + .frame(width: 42, height: 42) + .overlay(alignment: .center) { + if selectedColor == colorHex { + Image(systemName: "checkmark") + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(.white) + } + } + .overlay( + Circle() + .strokeBorder( + selectedColor == colorHex ? Theme.primaryText.opacity(0.4) : Color.clear, + lineWidth: 2 + ) + ) + .onTapGesture { selectedColor = colorHex } + } + } + .padding(.vertical, 8) + } + } + .navigationTitle(String(localized: "category.add")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(String(localized: "button.cancel")) { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button(String(localized: "button.add")) { + let trimmed = name.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { return } + onAdd(trimmed, selectedColor) + dismiss() + } + .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + } + } +} + +// MARK: - Color Hex Extension + +extension Color { + init(hex: String) { + let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let r = Double((int >> 16) & 0xFF) / 255 + let g = Double((int >> 8) & 0xFF) / 255 + let b = Double(int & 0xFF) / 255 + self.init(red: r, green: g, blue: b) + } +} + +#Preview { + NavigationStack { + CategoryView() + } +} diff --git a/myApp/LemonLimeTracker/IOS/Views/DashboardView.swift b/myApp/LemonLimeTracker/IOS/Views/DashboardView.swift new file mode 100644 index 0000000..d1c8d18 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/DashboardView.swift @@ -0,0 +1,160 @@ +import SwiftUI +import SwiftData + +struct DashboardView: View { + @Environment(\.modelContext) private var modelContext + @State private var viewModel = DashboardViewModel() + + var body: some View { + ZStack { + Theme.background.ignoresSafeArea() + ScrollView { + VStack(spacing: 20) { + progressRing + if !viewModel.topGoalProgress.isEmpty { + topGoals + } + remainingTasksSection + } + .padding() + } + } + .navigationTitle(String(localized: "tab.dashboard")) + .onAppear { viewModel.setup(context: modelContext) } + } + + // MARK: - Progress Ring + + private var progressRing: some View { + VStack(spacing: 12) { + ZStack { + Circle() + .stroke(Theme.surface, lineWidth: 16) + Circle() + .trim(from: 0, to: viewModel.overallProgress) + .stroke(Theme.green, style: StrokeStyle(lineWidth: 16, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeInOut(duration: 0.4), value: viewModel.overallProgress) + VStack(spacing: 4) { + Text("\(Int(viewModel.overallProgress * 100))%") + .font(.system(size: 34, weight: .bold, design: .rounded)) + .foregroundStyle(Theme.primaryText) + Text(String(localized: "dashboard.progress.title")) + .font(.caption) + .foregroundStyle(Theme.primaryText.opacity(0.6)) + } + } + .frame(width: 180, height: 180) + .padding(.top, 8) + } + } + + // MARK: - Top Goals + + private var topGoals: some View { + VStack(alignment: .leading, spacing: 10) { + Text(String(localized: "dashboard.goals.title")) + .font(.headline) + .foregroundStyle(Theme.primaryText) + + VStack(spacing: 10) { + ForEach(viewModel.topGoalProgress) { progress in + TopGoalCard(progress: progress) + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + // MARK: - Remaining Tasks + + private var remainingTasksSection: some View { + VStack(alignment: .leading, spacing: 10) { + Text(String(localized: "dashboard.tasks.title")) + .font(.headline) + .foregroundStyle(Theme.primaryText) + + if viewModel.remainingTasks.isEmpty { + Text(String(localized: "dashboard.tasks.empty")) + .font(.subheadline) + .foregroundStyle(Theme.primaryText.opacity(0.5)) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.vertical, 24) + } else { + VStack(spacing: 8) { + ForEach(viewModel.remainingTasks) { task in + RemainingTaskRow(task: task) { + viewModel.addCount(to: task) + } + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - Top Goal Card + +private struct TopGoalCard: View { + let progress: GoalProgress + + var body: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text(progress.displayName) + .font(.body) + .fontWeight(.semibold) + .foregroundStyle(Theme.primaryText) + ProgressView(value: progress.ratio) + .tint(progress.isCompleted ? Theme.green : Theme.yellow) + } + Text(progress.percentText) + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow) + } + .padding() + .background(Theme.surface, in: RoundedRectangle(cornerRadius: 12)) + } +} + +// MARK: - Remaining Task Row + +private struct RemainingTaskRow: View { + let task: TaskItem + let onAddCount: () -> Void + + var body: some View { + HStack(spacing: 12) { + Text(task.icon) + .font(.title3) + .frame(width: 30) + + Text(task.name) + .font(.body) + .foregroundStyle(Theme.primaryText) + + Spacer() + + if task.type == .count { + Button(action: onAddCount) { + Image(systemName: "plus.circle.fill") + .foregroundStyle(Theme.green) + } + .buttonStyle(.plain) + } else { + Image(systemName: "clock") + .foregroundStyle(Theme.primaryText.opacity(0.4)) + } + } + .padding(12) + .background(Theme.surface, in: RoundedRectangle(cornerRadius: 10)) + } +} + +#Preview { + NavigationStack { + DashboardView() + } +} diff --git a/myApp/LemonLimeTracker/IOS/Views/GoalView.swift b/myApp/LemonLimeTracker/IOS/Views/GoalView.swift new file mode 100644 index 0000000..8d60d24 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/GoalView.swift @@ -0,0 +1,306 @@ +import SwiftUI +import SwiftData +import Charts + +struct GoalView: View { + @Environment(\.modelContext) private var modelContext + @State private var viewModel = GoalViewModel() + @State private var showAddSheet = false + + var body: some View { + ZStack { + Theme.background.ignoresSafeArea() + Group { + if viewModel.goals.isEmpty { + emptyState + } else { + goalContent + } + } + } + .navigationTitle(String(localized: "tab.goals")) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { showAddSheet = true } label: { + Image(systemName: "plus") + .foregroundStyle(Theme.green) + } + } + } + .sheet(isPresented: $showAddSheet) { + AddGoalSheet { targetType, conditions, frequency, task, category in + viewModel.addGoal(targetType: targetType, conditions: conditions, frequency: frequency, task: task, category: category) + } + } + .onAppear { viewModel.setup(context: modelContext) } + } + + private var emptyState: some View { + VStack(spacing: 16) { + Image(systemName: "flag.badge.ellipsis") + .font(.system(size: 56)) + .foregroundStyle(Theme.green.opacity(0.6)) + Text(String(localized: "goal.empty")) + .foregroundStyle(Theme.primaryText.opacity(0.6)) + } + } + + private var goalContent: some View { + ScrollView { + VStack(spacing: 16) { + progressChart + .padding(.horizontal) + goalCards + .padding(.horizontal) + } + .padding(.vertical) + } + } + + // MARK: - Chart + + private var progressChart: some View { + VStack(alignment: .leading, spacing: 10) { + Text(String(localized: "goal.chart.title")) + .font(.headline) + .foregroundStyle(Theme.primaryText) + + Chart(viewModel.goalProgress) { progress in + BarMark( + x: .value(String(localized: "goal.progress"), progress.ratio * 100), + y: .value("", progress.displayName) + ) + .foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow) + .cornerRadius(4) + } + .chartXScale(domain: 0.0...100.0) + .chartXAxis { + AxisMarks(values: [0.0, 25.0, 50.0, 75.0, 100.0]) { value in + AxisGridLine() + AxisValueLabel { + if let v = value.as(Double.self) { + Text("\(Int(v))%") + .font(.caption2) + .foregroundStyle(Theme.primaryText.opacity(0.5)) + } + } + } + } + .chartYAxis { + AxisMarks { _ in + AxisValueLabel() + } + } + .frame(height: CGFloat(viewModel.goalProgress.count) * 48 + 20) + } + .padding() + .background(Theme.surface, in: RoundedRectangle(cornerRadius: 12)) + } + + // MARK: - Goal Cards + + private var goalCards: some View { + VStack(spacing: 12) { + ForEach(viewModel.goalProgress) { progress in + GoalCard(progress: progress) { + viewModel.deleteGoal(progress.goal) + } + } + } + } +} + +// MARK: - Goal Card + +private struct GoalCard: View { + let progress: GoalProgress + let onDelete: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 2) { + Text(progress.displayName) + .font(.body) + .fontWeight(.semibold) + .foregroundStyle(Theme.primaryText) + Text(subtitleText) + .font(.caption) + .foregroundStyle(Theme.primaryText.opacity(0.5)) + } + Spacer() + Button(role: .destructive, action: onDelete) { + Image(systemName: "trash") + .font(.caption) + .foregroundStyle(.red.opacity(0.7)) + } + .buttonStyle(.plain) + } + + ProgressView(value: progress.ratio) + .tint(progress.isCompleted ? Theme.green : Theme.yellow) + + HStack { + Text(currentText) + .font(.caption) + .foregroundStyle(Theme.primaryText.opacity(0.6)) + Spacer() + Text(progress.percentText) + .font(.caption) + .fontWeight(.bold) + .foregroundStyle(progress.isCompleted ? Theme.green : Theme.yellow) + } + } + .padding() + .background(Theme.surface, in: RoundedRectangle(cornerRadius: 12)) + } + + private var subtitleText: String { + let cond = progress.goal.targetType == "count" + ? String(localized: "goal.condition.count") + : String(localized: "goal.condition.time") + let freq: String + switch progress.goal.frequency { + case .daily: freq = String(localized: "goal.frequency.daily") + case .weekly: freq = String(localized: "goal.frequency.weekly") + case .monthly: freq = String(localized: "goal.frequency.monthly") + } + return "\(cond) · \(freq)" + } + + private var currentText: String { + if progress.goal.targetType == "count" { + return "\(Int(progress.current)) / \(Int(progress.target)) \(String(localized: "goal.condition.count"))" + } else { + let currentMins = Int(progress.current) / 60 + let targetMins = Int(progress.target) / 60 + return "\(currentMins)m / \(targetMins)m" + } + } +} + +// MARK: - Add Goal Sheet + +private struct AddGoalSheet: View { + @Environment(\.dismiss) private var dismiss + @Query(sort: \TaskItem.name) private var tasks: [TaskItem] + @Query(sort: \Category.name) private var categories: [Category] + + @State private var targetType: GoalTargetType = .task + @State private var selectedTask: TaskItem? + @State private var selectedCategory: Category? + @State private var conditionType: String = "count" + @State private var threshold: Double = 5 + @State private var frequency: GoalFrequency = .daily + + let onAdd: (String, Double, GoalFrequency, TaskItem?, Category?) -> Void + + var body: some View { + NavigationStack { + Form { + Section(header: Text(String(localized: "goal.target"))) { + Picker(String(localized: "goal.target"), selection: $targetType) { + Text(String(localized: "goal.target.task")).tag(GoalTargetType.task) + Text(String(localized: "goal.target.category")).tag(GoalTargetType.category) + } + .pickerStyle(.segmented) + .onChange(of: targetType) { _, _ in + selectedTask = nil + selectedCategory = nil + } + + if targetType == .task { + Picker(String(localized: "goal.target.task"), selection: $selectedTask) { + Text(String(localized: "goal.no.target")).tag(Optional.none) + ForEach(tasks) { task in + Text("\(task.icon) \(task.name)").tag(Optional(task)) + } + } + } else { + Picker(String(localized: "goal.target.category"), selection: $selectedCategory) { + Text(String(localized: "goal.no.target")).tag(Optional.none) + ForEach(categories) { cat in + Text(cat.name).tag(Optional(cat)) + } + } + } + } + + Section(header: Text(String(localized: "goal.condition"))) { + Picker(String(localized: "goal.condition"), selection: $conditionType) { + Text(String(localized: "goal.condition.count")).tag("count") + Text(String(localized: "goal.condition.time")).tag("duration") + } + .pickerStyle(.segmented) + .onChange(of: conditionType) { _, _ in threshold = 5 } + } + + Section(header: Text(String(localized: "goal.threshold"))) { + HStack { + Slider( + value: $threshold, + in: conditionType == "count" ? 1.0...100.0 : 5.0...240.0, + step: conditionType == "count" ? 1.0 : 5.0 + ) + .tint(Theme.green) + Text(thresholdText) + .font(.body) + .monospacedDigit() + .foregroundStyle(Theme.green) + .frame(width: 52, alignment: .trailing) + } + } + + Section(header: Text(String(localized: "goal.frequency"))) { + Picker(String(localized: "goal.frequency"), selection: $frequency) { + Text(String(localized: "goal.frequency.daily")).tag(GoalFrequency.daily) + Text(String(localized: "goal.frequency.weekly")).tag(GoalFrequency.weekly) + Text(String(localized: "goal.frequency.monthly")).tag(GoalFrequency.monthly) + } + .pickerStyle(.segmented) + } + } + .navigationTitle(String(localized: "goal.add")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(String(localized: "button.cancel")) { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button(String(localized: "button.add")) { + let storedConditions = conditionType == "duration" ? threshold * 60.0 : threshold + onAdd( + conditionType, + storedConditions, + frequency, + targetType == .task ? selectedTask : nil, + targetType == .category ? selectedCategory : nil + ) + dismiss() + } + .disabled(!canAdd) + } + } + } + } + + private var thresholdText: String { + if conditionType == "count" { + return "\(Int(threshold))" + } else { + let h = Int(threshold) / 60 + let m = Int(threshold) % 60 + return h > 0 ? "\(h)h\(m)m" : "\(Int(threshold))m" + } + } + + private var canAdd: Bool { + targetType == .task ? selectedTask != nil : selectedCategory != nil + } +} + +#Preview { + NavigationStack { + GoalView() + } +} diff --git a/myApp/LemonLimeTracker/IOS/Views/LaunchScreenView.swift b/myApp/LemonLimeTracker/IOS/Views/LaunchScreenView.swift new file mode 100644 index 0000000..7e198e0 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/LaunchScreenView.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct LaunchScreenView: View { + var body: some View { + ZStack { + Theme.background + .ignoresSafeArea() + VStack(spacing: 12) { + Text("🍋🍈") + .font(.system(size: 60)) + Text("LemonLimeTracker") + .font(.largeTitle) + .fontWeight(.bold) + .foregroundStyle(Theme.green) + } + } + } +} + +#Preview { + LaunchScreenView() +} diff --git a/myApp/LemonLimeTracker/IOS/Views/MainTabView.swift b/myApp/LemonLimeTracker/IOS/Views/MainTabView.swift new file mode 100644 index 0000000..323770f --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/MainTabView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct MainTabView: View { + var body: some View { + TabView { + NavigationStack { + DashboardView() + } + .tabItem { + Label(String(localized: "tab.dashboard"), systemImage: "chart.bar.fill") + } + + NavigationStack { + CategoryView() + } + .tabItem { + Label(String(localized: "tab.category"), systemImage: "folder.fill") + } + + NavigationStack { + TaskTrackerView() + } + .tabItem { + Label(String(localized: "tab.tasks"), systemImage: "checkmark.circle.fill") + } + + NavigationStack { + GoalView() + } + .tabItem { + Label(String(localized: "tab.goals"), systemImage: "flag.fill") + } + + NavigationStack { + SettingsView() + } + .tabItem { + Label(String(localized: "tab.settings"), systemImage: "gearshape.fill") + } + } + .tint(Theme.green) + } +} + +#Preview { + MainTabView() +} diff --git a/myApp/LemonLimeTracker/IOS/Views/RootView.swift b/myApp/LemonLimeTracker/IOS/Views/RootView.swift new file mode 100644 index 0000000..c535012 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/RootView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct RootView: View { + @State private var isLaunching = true + + var body: some View { + if isLaunching { + LaunchScreenView() + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation(.easeInOut(duration: 0.4)) { + isLaunching = false + } + } + } + } else { + MainTabView() + } + } +} + +#Preview { + RootView() +} diff --git a/myApp/LemonLimeTracker/IOS/Views/SettingsView.swift b/myApp/LemonLimeTracker/IOS/Views/SettingsView.swift new file mode 100644 index 0000000..4c81cc0 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/SettingsView.swift @@ -0,0 +1,107 @@ +import SwiftUI + +struct SettingsView: View { + @State private var viewModel = SettingsViewModel() + + var body: some View { + Form { + Section(header: Text(String(localized: "settings.section.general"))) { + Picker(String(localized: "settings.language"), selection: $viewModel.language) { + ForEach(AppLanguage.allCases) { lang in + Text(lang.displayName).tag(lang) + } + } + + Picker(String(localized: "settings.firstWeekday"), selection: $viewModel.firstWeekday) { + ForEach(FirstWeekday.allCases) { day in + Text(day.displayName).tag(day) + } + } + + DatePicker( + String(localized: "settings.dayStart"), + selection: $viewModel.dayStartDate, + displayedComponents: .hourAndMinute + ) + .tint(Theme.green) + } + .listRowBackground(Theme.surface) + + Section { + ForEach(PremiumFeature.allCases) { feature in + PremiumFeatureRow(feature: feature) + } + } header: { + Text(String(localized: "settings.section.premium")) + } footer: { + Text(String(localized: "settings.premium.footnote")) + } + .listRowBackground(Theme.surface) + } + .scrollContentBackground(.hidden) + .background(Theme.background.ignoresSafeArea()) + .navigationTitle(String(localized: "tab.settings")) + } +} + +// MARK: - Premium Feature + +private enum PremiumFeature: String, CaseIterable, Identifiable { + case cloudSync + case appleWatch + case ipadMacSync + case widgets + case siriShortcuts + + var id: String { rawValue } + + var icon: String { + switch self { + case .cloudSync: return "icloud.fill" + case .appleWatch: return "applewatch" + case .ipadMacSync: return "ipad.and.macbook" + case .widgets: return "square.grid.2x2.fill" + case .siriShortcuts: return "mic.fill" + } + } + + var titleKey: String { + switch self { + case .cloudSync: return "settings.premium.cloudSync" + case .appleWatch: return "settings.premium.appleWatch" + case .ipadMacSync: return "settings.premium.ipadMacSync" + case .widgets: return "settings.premium.widgets" + case .siriShortcuts: return "settings.premium.siriShortcuts" + } + } +} + +private struct PremiumFeatureRow: View { + let feature: PremiumFeature + + var body: some View { + HStack(spacing: 12) { + Image(systemName: feature.icon) + .font(.body) + .foregroundStyle(Theme.primaryText.opacity(0.35)) + .frame(width: 24) + + Text(String(localized: String.LocalizationValue(feature.titleKey))) + .font(.body) + .foregroundStyle(Theme.primaryText.opacity(0.35)) + + Spacer() + + Image(systemName: "lock.fill") + .font(.caption) + .foregroundStyle(Theme.yellow.opacity(0.8)) + } + .opacity(0.7) + } +} + +#Preview { + NavigationStack { + SettingsView() + } +} diff --git a/myApp/LemonLimeTracker/IOS/Views/TaskTrackerView.swift b/myApp/LemonLimeTracker/IOS/Views/TaskTrackerView.swift new file mode 100644 index 0000000..2869bd5 --- /dev/null +++ b/myApp/LemonLimeTracker/IOS/Views/TaskTrackerView.swift @@ -0,0 +1,212 @@ +import SwiftUI +import SwiftData + +struct TaskTrackerView: View { + @Environment(\.modelContext) private var modelContext + @State private var viewModel = TaskViewModel() + @State private var showAddSheet = false + + var body: some View { + ZStack { + Theme.background.ignoresSafeArea() + Group { + if viewModel.tasks.isEmpty { + emptyState + } else { + taskList + } + } + } + .navigationTitle(String(localized: "tab.tasks")) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + showAddSheet = true + } label: { + Image(systemName: "plus") + .foregroundStyle(Theme.green) + } + } + } + .sheet(isPresented: $showAddSheet) { + AddTaskSheet { name, icon, type, category in + viewModel.addTask(name: name, icon: icon, type: type, category: category) + } + } + .onAppear { + viewModel.setup(context: modelContext) + } + } + + private var emptyState: some View { + VStack(spacing: 16) { + Image(systemName: "checkmark.circle.badge.plus") + .font(.system(size: 56)) + .foregroundStyle(Theme.green.opacity(0.6)) + Text(String(localized: "task.empty")) + .foregroundStyle(Theme.primaryText.opacity(0.6)) + } + } + + private var taskList: some View { + List { + ForEach(viewModel.tasks) { task in + TaskRow(task: task, viewModel: viewModel) + .listRowBackground(Theme.surface) + .swipeActions(edge: .trailing, allowsFullSwipe: false) { + Button(role: .destructive) { + viewModel.deleteTask(task) + } label: { + Label(String(localized: "task.delete"), systemImage: "trash") + } + } + .swipeActions(edge: .leading, allowsFullSwipe: true) { + if task.type == .count { + Button { + viewModel.addCount(to: task) + } label: { + Label(String(localized: "task.add.count"), systemImage: "plus") + } + .tint(Theme.green) + } + } + } + } + .listStyle(.plain) + .scrollContentBackground(.hidden) + } +} + +// MARK: - Task Row + +private struct TaskRow: View { + let task: TaskItem + let viewModel: TaskViewModel + + private var isRunning: Bool { viewModel.isTimerRunning(for: task.id) } + + var body: some View { + HStack(spacing: 12) { + Text(task.icon) + .font(.title2) + .frame(width: 36) + + VStack(alignment: .leading, spacing: 2) { + Text(task.name) + .font(.body) + .fontWeight(.medium) + .foregroundStyle(Theme.primaryText) + if let category = task.category { + Text(category.name) + .font(.caption2) + .foregroundStyle(Theme.green) + } + } + + Spacer() + + if task.type == .time { + timeControls + } else { + countBadge + } + } + .padding(.vertical, 4) + } + + private var timeControls: some View { + HStack(spacing: 8) { + if isRunning { + Text(viewModel.formattedElapsed(for: task.id)) + .font(.system(.subheadline, design: .monospaced)) + .foregroundStyle(Theme.green) + .contentTransition(.numericText()) + .animation(.linear(duration: 0.3), value: viewModel.elapsedSeconds[task.id]) + } + Button { + viewModel.toggleTimer(for: task) + } label: { + Image(systemName: isRunning ? "stop.fill" : "play.fill") + .font(.title3) + .foregroundStyle(isRunning ? Theme.yellow : Theme.green) + .frame(width: 36, height: 36) + .background( + Circle() + .fill(isRunning ? Theme.yellow.opacity(0.15) : Theme.green.opacity(0.15)) + ) + } + .buttonStyle(.plain) + } + } + + private var countBadge: some View { + let count = viewModel.todayCount(for: task) + return Text("×\(count)") + .font(.headline) + .monospacedDigit() + .foregroundStyle(count > 0 ? Theme.green : Theme.primaryText.opacity(0.3)) + } +} + +// MARK: - Add Task Sheet + +private struct AddTaskSheet: View { + @Environment(\.dismiss) private var dismiss + @Query private var categories: [Category] + + @State private var name = "" + @State private var icon = "⭐" + @State private var type: TaskType = .count + @State private var selectedCategory: Category? + + let onAdd: (String, String, TaskType, Category?) -> Void + + var body: some View { + NavigationStack { + Form { + Section { + TextField(String(localized: "task.name"), text: $name) + TextField(String(localized: "task.icon"), text: $icon) + } + Section { + Picker(String(localized: "task.type"), selection: $type) { + Text(String(localized: "task.type.count")).tag(TaskType.count) + Text(String(localized: "task.type.time")).tag(TaskType.time) + } + .pickerStyle(.segmented) + } + if !categories.isEmpty { + Section { + Picker(String(localized: "task.category"), selection: $selectedCategory) { + Text(String(localized: "task.category.none")).tag(Optional.none) + ForEach(categories) { cat in + Text(cat.name).tag(Optional(cat)) + } + } + } + } + } + .navigationTitle(String(localized: "task.add")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button(String(localized: "button.cancel")) { dismiss() } + } + ToolbarItem(placement: .confirmationAction) { + Button(String(localized: "button.add")) { + let trimmed = name.trimmingCharacters(in: .whitespaces) + guard !trimmed.isEmpty else { return } + let resolvedIcon = icon.trimmingCharacters(in: .whitespaces).isEmpty ? "⭐" : icon + onAdd(trimmed, resolvedIcon, type, selectedCategory) + dismiss() + } + .disabled(name.trimmingCharacters(in: .whitespaces).isEmpty) + } + } + } + } +} + +#Preview { + TaskTrackerView() +} diff --git a/myApp/LemonLimeTracker/LemonLimeTracker-Info.plist b/myApp/LemonLimeTracker/LemonLimeTracker-Info.plist new file mode 100644 index 0000000..c41d6c0 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeTracker-Info.plist @@ -0,0 +1,13 @@ + + + + + CFBundleLocalizations + + ko + en + + NSSupportsLiveActivities + + + diff --git a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj new file mode 100644 index 0000000..01f3c85 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj @@ -0,0 +1,543 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 7280EB402FE41F11006B83D9 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7280EB3F2FE41F10006B83D9 /* WidgetKit.framework */; }; + 7280EB422FE41F11006B83D9 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7280EB412FE41F11006B83D9 /* SwiftUI.framework */; }; + 7280EB4F2FE41F12006B83D9 /* LemonLimeWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 7280EB3D2FE41F10006B83D9 /* LemonLimeWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 7280EB4D2FE41F12006B83D9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7280EAF02FE40D38006B83D9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7280EB3C2FE41F10006B83D9; + remoteInfo = LemonLimeWidgetExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 7280EB542FE41F12006B83D9 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 7280EB4F2FE41F12006B83D9 /* LemonLimeWidgetExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 7280EAF82FE40D38006B83D9 /* LemonLimeTracker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LemonLimeTracker.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7280EB1E2FE41A14006B83D9 /* LemonLimeTracker-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "LemonLimeTracker-Info.plist"; sourceTree = ""; }; + 7280EB3D2FE41F10006B83D9 /* LemonLimeWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = LemonLimeWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 7280EB3F2FE41F10006B83D9 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 7280EB412FE41F11006B83D9 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 7280EB502FE41F12006B83D9 /* Exceptions for "LemonLimeWidget" folder in "LemonLimeWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 7280EB3C2FE41F10006B83D9 /* LemonLimeWidgetExtension */; + }; + 7280EB5A2FE42219006B83D9 /* Exceptions for "IOS" folder in "LemonLimeWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Core/TimerActivityAttributes.swift, + ); + target = 7280EB3C2FE41F10006B83D9 /* LemonLimeWidgetExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 7280EAFA2FE40D38006B83D9 /* IOS */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7280EB5A2FE42219006B83D9 /* Exceptions for "IOS" folder in "LemonLimeWidgetExtension" target */, + ); + path = IOS; + sourceTree = ""; + }; + 7280EB432FE41F11006B83D9 /* LemonLimeWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 7280EB502FE41F12006B83D9 /* Exceptions for "LemonLimeWidget" folder in "LemonLimeWidgetExtension" target */, + ); + path = LemonLimeWidget; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7280EAF52FE40D38006B83D9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7280EB3A2FE41F10006B83D9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7280EB422FE41F11006B83D9 /* SwiftUI.framework in Frameworks */, + 7280EB402FE41F11006B83D9 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7280EAEF2FE40D38006B83D9 = { + isa = PBXGroup; + children = ( + 7280EB1E2FE41A14006B83D9 /* LemonLimeTracker-Info.plist */, + 7280EAFA2FE40D38006B83D9 /* IOS */, + 7280EB432FE41F11006B83D9 /* LemonLimeWidget */, + 7280EB3E2FE41F10006B83D9 /* Frameworks */, + 7280EAF92FE40D38006B83D9 /* Products */, + ); + sourceTree = ""; + }; + 7280EAF92FE40D38006B83D9 /* Products */ = { + isa = PBXGroup; + children = ( + 7280EAF82FE40D38006B83D9 /* LemonLimeTracker.app */, + 7280EB3D2FE41F10006B83D9 /* LemonLimeWidgetExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 7280EB3E2FE41F10006B83D9 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7280EB3F2FE41F10006B83D9 /* WidgetKit.framework */, + 7280EB412FE41F11006B83D9 /* SwiftUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7280EAF72FE40D38006B83D9 /* LemonLimeTracker */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7280EB032FE40D39006B83D9 /* Build configuration list for PBXNativeTarget "LemonLimeTracker" */; + buildPhases = ( + 7280EAF42FE40D38006B83D9 /* Sources */, + 7280EAF52FE40D38006B83D9 /* Frameworks */, + 7280EAF62FE40D38006B83D9 /* Resources */, + 7280EB542FE41F12006B83D9 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 7280EB4E2FE41F12006B83D9 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7280EAFA2FE40D38006B83D9 /* IOS */, + ); + name = LemonLimeTracker; + packageProductDependencies = ( + ); + productName = LemonLimeTracker; + productReference = 7280EAF82FE40D38006B83D9 /* LemonLimeTracker.app */; + productType = "com.apple.product-type.application"; + }; + 7280EB3C2FE41F10006B83D9 /* LemonLimeWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7280EB512FE41F12006B83D9 /* Build configuration list for PBXNativeTarget "LemonLimeWidgetExtension" */; + buildPhases = ( + 7280EB392FE41F10006B83D9 /* Sources */, + 7280EB3A2FE41F10006B83D9 /* Frameworks */, + 7280EB3B2FE41F10006B83D9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 7280EB432FE41F11006B83D9 /* LemonLimeWidget */, + ); + name = LemonLimeWidgetExtension; + packageProductDependencies = ( + ); + productName = LemonLimeWidgetExtension; + productReference = 7280EB3D2FE41F10006B83D9 /* LemonLimeWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7280EAF02FE40D38006B83D9 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2650; + LastUpgradeCheck = 2650; + TargetAttributes = { + 7280EAF72FE40D38006B83D9 = { + CreatedOnToolsVersion = 26.5; + }; + 7280EB3C2FE41F10006B83D9 = { + CreatedOnToolsVersion = 26.5; + }; + }; + }; + buildConfigurationList = 7280EAF32FE40D38006B83D9 /* Build configuration list for PBXProject "LemonLimeTracker" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ko, + ); + mainGroup = 7280EAEF2FE40D38006B83D9; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 7280EAF92FE40D38006B83D9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7280EAF72FE40D38006B83D9 /* LemonLimeTracker */, + 7280EB3C2FE41F10006B83D9 /* LemonLimeWidgetExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7280EAF62FE40D38006B83D9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7280EB3B2FE41F10006B83D9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7280EAF42FE40D38006B83D9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7280EB392FE41F10006B83D9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 7280EB4E2FE41F12006B83D9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7280EB3C2FE41F10006B83D9 /* LemonLimeWidgetExtension */; + targetProxy = 7280EB4D2FE41F12006B83D9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 7280EB012FE40D39006B83D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7280EB022FE40D39006B83D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7280EB042FE40D39006B83D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LemonLimeTracker-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_LSApplicationCategoryType = ""; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.LemonLimeTracker; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7280EB052FE40D39006B83D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "LemonLimeTracker-Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_LSApplicationCategoryType = ""; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.LemonLimeTracker; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7280EB522FE41F12006B83D9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LemonLimeWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LemonLimeWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.LemonLimeTracker.LemonLimeWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7280EB532FE41F12006B83D9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = LemonLimeWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = LemonLimeWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.yechan.LemonLimeTracker.LemonLimeWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7280EAF32FE40D38006B83D9 /* Build configuration list for PBXProject "LemonLimeTracker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7280EB012FE40D39006B83D9 /* Debug */, + 7280EB022FE40D39006B83D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7280EB032FE40D39006B83D9 /* Build configuration list for PBXNativeTarget "LemonLimeTracker" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7280EB042FE40D39006B83D9 /* Debug */, + 7280EB052FE40D39006B83D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7280EB512FE41F12006B83D9 /* Build configuration list for PBXNativeTarget "LemonLimeWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7280EB522FE41F12006B83D9 /* Debug */, + 7280EB532FE41F12006B83D9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7280EAF02FE40D38006B83D9 /* Project object */; +} diff --git a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..2853e57128c5a6cdd3c6f31020b51fa7096c942f GIT binary patch literal 35877 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 literal 0 HcmV?d00001 diff --git a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..cf0295c --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,19 @@ + + + + + SchemeUserState + + LemonLimeTracker.xcscheme_^#shared#^_ + + orderHint + 0 + + LemonLimeWidgetExtension.xcscheme_^#shared#^_ + + orderHint + 1 + + + + diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/Contents.json b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/Info.plist b/myApp/LemonLimeTracker/LemonLimeWidget/Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift new file mode 100644 index 0000000..f5d0984 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift @@ -0,0 +1,84 @@ +// +// LemonLimeWidget.swift +// LemonLimeWidget +// +// Created by 송예찬 on 6/18/26. +// + +import WidgetKit +import SwiftUI + +struct Provider: TimelineProvider { + func placeholder(in context: Context) -> SimpleEntry { + SimpleEntry(date: Date(), emoji: "😀") + } + + func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) { + let entry = SimpleEntry(date: Date(), emoji: "😀") + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { + var entries: [SimpleEntry] = [] + + // Generate a timeline consisting of five entries an hour apart, starting from the current date. + let currentDate = Date() + for hourOffset in 0 ..< 5 { + let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)! + let entry = SimpleEntry(date: entryDate, emoji: "😀") + entries.append(entry) + } + + let timeline = Timeline(entries: entries, policy: .atEnd) + completion(timeline) + } + +// func relevances() async -> WidgetRelevances { +// // Generate a list containing the contexts this widget is relevant in. +// } +} + +struct SimpleEntry: TimelineEntry { + let date: Date + let emoji: String +} + +struct LemonLimeWidgetEntryView : View { + var entry: Provider.Entry + + var body: some View { + VStack { + Text("Time:") + Text(entry.date, style: .time) + + Text("Emoji:") + Text(entry.emoji) + } + } +} + +struct LemonLimeWidget: Widget { + let kind: String = "LemonLimeWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: Provider()) { entry in + if #available(iOS 17.0, *) { + LemonLimeWidgetEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } else { + LemonLimeWidgetEntryView(entry: entry) + .padding() + .background() + } + } + .configurationDisplayName("My Widget") + .description("This is an example widget.") + } +} + +#Preview(as: .systemSmall) { + LemonLimeWidget() +} timeline: { + SimpleEntry(date: .now, emoji: "😀") + SimpleEntry(date: .now, emoji: "🤩") +} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetBundle.swift b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetBundle.swift new file mode 100644 index 0000000..8b40784 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetBundle.swift @@ -0,0 +1,17 @@ +// +// LemonLimeWidgetBundle.swift +// LemonLimeWidget +// +// Created by 송예찬 on 6/18/26. +// + +import WidgetKit +import SwiftUI + +@main +struct LemonLimeWidgetBundle: WidgetBundle { + var body: some Widget { + LemonLimeWidget() + LemonLimeWidgetLiveActivity() + } +} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetLiveActivity.swift b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetLiveActivity.swift new file mode 100644 index 0000000..594a7e8 --- /dev/null +++ b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetLiveActivity.swift @@ -0,0 +1,125 @@ +// +// LemonLimeWidgetLiveActivity.swift +// LemonLimeWidget +// +// Created by 송예찬 on 6/18/26. +// + +import ActivityKit +import WidgetKit +import SwiftUI + +private let timerEndDate = Date.distantFuture + +struct LemonLimeWidgetLiveActivity: Widget { + var body: some WidgetConfiguration { + ActivityConfiguration(for: TimerActivityAttributes.self) { context in + // MARK: Lock Screen / Banner + HStack(spacing: 14) { + Image(systemName: "timer") + .font(.title2.weight(.semibold)) + .foregroundStyle(.yellow) + + VStack(alignment: .leading, spacing: 3) { + Text(context.attributes.taskName) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.white) + .lineLimit(1) + + Text(timerInterval: context.state.startDate...timerEndDate, countsDown: false) + .font(.caption.monospacedDigit()) + .foregroundStyle(.yellow) + } + + Spacer() + + Text(context.attributes.taskIcon) + .font(.largeTitle) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .activityBackgroundTint(Color(red: 0.08, green: 0.30, blue: 0.08)) + .activitySystemActionForegroundColor(.yellow) + + } dynamicIsland: { context in + DynamicIsland { + // MARK: Expanded – top row + DynamicIslandExpandedRegion(.leading) { + Label { + Text(context.attributes.taskName) + .font(.caption.weight(.semibold)) + .lineLimit(1) + } icon: { + Text(context.attributes.taskIcon) + .font(.caption) + } + .foregroundStyle(.green) + } + + DynamicIslandExpandedRegion(.trailing) { + Image(systemName: "circle.fill") + .font(.caption) + .foregroundStyle(.green) + .padding(.trailing, 4) + } + + // MARK: Expanded – center (large timer) + DynamicIslandExpandedRegion(.bottom) { + Text(timerInterval: context.state.startDate...timerEndDate, countsDown: false) + .font(.system(size: 44, weight: .bold, design: .monospaced)) + .foregroundStyle(.yellow) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 4) + } + + } compactLeading: { + Image(systemName: "timer") + .foregroundStyle(.green) + .font(.caption.weight(.semibold)) + + } compactTrailing: { + Text(timerInterval: context.state.startDate...timerEndDate, countsDown: false) + .monospacedDigit() + .font(.caption2.weight(.medium)) + .foregroundStyle(.yellow) + .frame(maxWidth: 52) + + } minimal: { + ZStack { + Circle() + .fill(LinearGradient( + colors: [.green, Color(red: 0.8, green: 0.9, blue: 0.0)], + startPoint: .topLeading, + endPoint: .bottomTrailing + )) + Text(context.attributes.taskIcon) + .font(.system(size: 11)) + } + } + .keylineTint(.green) + } + } +} + +// MARK: - Previews + +extension TimerActivityAttributes { + fileprivate static var preview: TimerActivityAttributes { + TimerActivityAttributes(taskName: "운동하기", taskIcon: "🏃") + } +} + +extension TimerActivityAttributes.ContentState { + fileprivate static var running: TimerActivityAttributes.ContentState { + TimerActivityAttributes.ContentState( + startDate: Date().addingTimeInterval(-125), + elapsedSeconds: 125 + ) + } +} + +#Preview("Notification", as: .content, using: TimerActivityAttributes.preview) { + LemonLimeWidgetLiveActivity() +} contentStates: { + TimerActivityAttributes.ContentState.running +}