diff --git a/myApp/LemonLimeTracker/CLAUDE.md b/myApp/LemonLimeTracker/CLAUDE.md deleted file mode 100644 index 00a18fe..0000000 --- a/myApp/LemonLimeTracker/CLAUDE.md +++ /dev/null @@ -1,21 +0,0 @@ -# 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 deleted file mode 100644 index eb87897..0000000 --- a/myApp/LemonLimeTracker/IOS/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 2305880..0000000 --- a/myApp/LemonLimeTracker/IOS/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 deleted file mode 100644 index 73c0059..0000000 --- a/myApp/LemonLimeTracker/IOS/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/myApp/LemonLimeTracker/IOS/ContentView.swift b/myApp/LemonLimeTracker/IOS/ContentView.swift deleted file mode 100644 index e245bef..0000000 --- a/myApp/LemonLimeTracker/IOS/ContentView.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// 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 deleted file mode 100644 index a62070a..0000000 --- a/myApp/LemonLimeTracker/IOS/Core/Theme.swift +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 4b3f522..0000000 --- a/myApp/LemonLimeTracker/IOS/Core/TimerActivityAttributes.swift +++ /dev/null @@ -1,14 +0,0 @@ -#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 deleted file mode 100644 index 434cfec..0000000 --- a/myApp/LemonLimeTracker/IOS/LemonLimeTrackerApp.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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 deleted file mode 100644 index 79339cb..0000000 --- a/myApp/LemonLimeTracker/IOS/Models/Category.swift +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 959e939..0000000 --- a/myApp/LemonLimeTracker/IOS/Models/Goal.swift +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index bbe039e..0000000 --- a/myApp/LemonLimeTracker/IOS/Models/TaskItem.swift +++ /dev/null @@ -1,28 +0,0 @@ -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 deleted file mode 100644 index ccd5273..0000000 --- a/myApp/LemonLimeTracker/IOS/Models/TaskLog.swift +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index b056f1e..0000000 --- a/myApp/LemonLimeTracker/IOS/Repositories/SwiftDataService.swift +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 0d88802..0000000 --- a/myApp/LemonLimeTracker/IOS/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,72 +0,0 @@ -/* 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 deleted file mode 100644 index 6e2b31d..0000000 --- a/myApp/LemonLimeTracker/IOS/Resources/ko.lproj/Localizable.strings +++ /dev/null @@ -1,72 +0,0 @@ -/* 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 deleted file mode 100644 index a7a858b..0000000 --- a/myApp/LemonLimeTracker/IOS/ViewModels/CategoryViewModel.swift +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 85a12a5..0000000 --- a/myApp/LemonLimeTracker/IOS/ViewModels/DashboardViewModel.swift +++ /dev/null @@ -1,84 +0,0 @@ -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 deleted file mode 100644 index 196b392..0000000 --- a/myApp/LemonLimeTracker/IOS/ViewModels/GoalViewModel.swift +++ /dev/null @@ -1,93 +0,0 @@ -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 deleted file mode 100644 index cb5ec61..0000000 --- a/myApp/LemonLimeTracker/IOS/ViewModels/SettingsViewModel.swift +++ /dev/null @@ -1,76 +0,0 @@ -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 deleted file mode 100644 index 0923425..0000000 --- a/myApp/LemonLimeTracker/IOS/ViewModels/TaskViewModel.swift +++ /dev/null @@ -1,157 +0,0 @@ -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 deleted file mode 100644 index 2b6369f..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/CategoryView.swift +++ /dev/null @@ -1,177 +0,0 @@ -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 deleted file mode 100644 index d1c8d18..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/DashboardView.swift +++ /dev/null @@ -1,160 +0,0 @@ -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 deleted file mode 100644 index 8d60d24..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/GoalView.swift +++ /dev/null @@ -1,306 +0,0 @@ -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 deleted file mode 100644 index 7e198e0..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/LaunchScreenView.swift +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 323770f..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/MainTabView.swift +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index c535012..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/RootView.swift +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index 4c81cc0..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/SettingsView.swift +++ /dev/null @@ -1,107 +0,0 @@ -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 deleted file mode 100644 index 2869bd5..0000000 --- a/myApp/LemonLimeTracker/IOS/Views/TaskTrackerView.swift +++ /dev/null @@ -1,212 +0,0 @@ -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 deleted file mode 100644 index c41d6c0..0000000 --- a/myApp/LemonLimeTracker/LemonLimeTracker-Info.plist +++ /dev/null @@ -1,13 +0,0 @@ - - - - - CFBundleLocalizations - - ko - en - - NSSupportsLiveActivities - - - diff --git a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj deleted file mode 100644 index 01f3c85..0000000 --- a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.pbxproj +++ /dev/null @@ -1,543 +0,0 @@ -// !$*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 deleted file mode 100644 index 919434a..0000000 --- a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - 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 deleted file mode 100644 index 2853e57..0000000 Binary files a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/project.xcworkspace/xcuserdata/ceuak.xcuserdatad/UserInterfaceState.xcuserstate and /dev/null differ diff --git a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist b/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index cf0295c..0000000 --- a/myApp/LemonLimeTracker/LemonLimeTracker.xcodeproj/xcuserdata/ceuak.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,19 +0,0 @@ - - - - - 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 deleted file mode 100644 index eb87897..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 2305880..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 deleted file mode 100644 index 73c0059..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "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 deleted file mode 100644 index eb87897..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/Info.plist b/myApp/LemonLimeTracker/LemonLimeWidget/Info.plist deleted file mode 100644 index 0f118fb..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/Info.plist +++ /dev/null @@ -1,11 +0,0 @@ - - - - - NSExtension - - NSExtensionPointIdentifier - com.apple.widgetkit-extension - - - diff --git a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift b/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift deleted file mode 100644 index f5d0984..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidget.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// 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 deleted file mode 100644 index 8b40784..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetBundle.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// 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 deleted file mode 100644 index 594a7e8..0000000 --- a/myApp/LemonLimeTracker/LemonLimeWidget/LemonLimeWidgetLiveActivity.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// 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 -}