commit 9dafdce85522e39339a03f22362dd48b66866cbf Author: loveuer Date: Wed Sep 2 00:27:33 2026 -0700 feat: add macOS task board widget app diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..078406b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +DerivedData/ +build/ +xcuserdata/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a199eb --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# UTrello + +一个运行在 macOS 桌面的本地 Trello 风格任务看板,附带 WidgetKit 小组件。 + +## 功能 + +- 三列任务看板:待办、进行中、完成 +- 新增、移动和删除任务 +- 桌面小组件显示未完成任务及其数量 +- 任务数据只保存在本机;主应用和 Widget 通过 App Group 共享数据 +- 主应用更新任务后,主动请求系统刷新小组件 + +## 技术要求 + +- macOS 14 Sonoma 或更高版本 +- Xcode 15 或更高版本 +- 可用于代码签名的 Apple 开发团队 + +## 本地运行 + +1. 在 Mac 上用 Xcode 打开 `UTrello.xcodeproj`。 +2. 选择 `UTrello` target,在 **Signing & Capabilities** 中选择开发团队。 +3. 对 `UTrelloWidget` target 重复上一步。 +4. 在两个 target 的 App Groups capability 中,将 `group.com.example.utrello` 改为该开发团队已注册的 App Group 标识。 +5. 将 [TaskStore.swift](UTrello/Shared/TaskStore.swift) 中的 `groupIdentifier` 改为同一个标识。 +6. 选择 `UTrello` scheme 后运行。首次运行成功后,在 macOS 的组件库中将 **UTrello Tasks** 添加到桌面。 + +## 数据与刷新 + +任务以 JSON 形式写入 App Group 的 `UserDefaults`。Widget 会在任务变更时请求刷新,并会由系统每 15 分钟兜底刷新一次;实际刷新时机仍由 macOS 调度。 + +## 当前限制 + +- 这是纯本地应用,不会同步真实 Trello 服务。 +- Widget 以浏览为主,复杂操作在主应用内完成。 diff --git a/UTrello.xcodeproj/project.pbxproj b/UTrello.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f3d2af6 --- /dev/null +++ b/UTrello.xcodeproj/project.pbxproj @@ -0,0 +1,111 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = {}; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A10000000000000000000001 /* UTrelloApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000011 /* UTrelloApp.swift */; }; + A10000000000000000000002 /* BoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000012 /* BoardView.swift */; }; + A10000000000000000000003 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000013 /* TaskItem.swift */; }; + A10000000000000000000004 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000014 /* TaskStore.swift */; }; + A10000000000000000000005 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10000000000000000000021 /* WidgetKit.framework */; }; + A10000000000000000000006 /* UTrelloWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000031 /* UTrelloWidget.swift */; }; + A10000000000000000000007 /* UTrelloWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000032 /* UTrelloWidgetBundle.swift */; }; + A10000000000000000000008 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000013 /* TaskItem.swift */; }; + A10000000000000000000009 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10000000000000000000014 /* TaskStore.swift */; }; + A1000000000000000000000A /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A10000000000000000000021 /* WidgetKit.framework */; }; + A1000000000000000000000B /* UTrelloWidget.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = A10000000000000000000041 /* UTrelloWidget.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A10000000000000000000051 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = A10000000000000000000061 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A10000000000000000000071; + remoteInfo = UTrelloWidget; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + A10000000000000000000081 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = (A1000000000000000000000B /* UTrelloWidget.appex in Embed App Extensions */, ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + A10000000000000000000011 /* UTrelloApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTrelloApp.swift; sourceTree = ""; }; + A10000000000000000000012 /* BoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoardView.swift; sourceTree = ""; }; + A10000000000000000000013 /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = ""; }; + A10000000000000000000014 /* TaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStore.swift; sourceTree = ""; }; + A10000000000000000000015 /* UTrello.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = UTrello.entitlements; sourceTree = ""; }; + A10000000000000000000021 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + A10000000000000000000031 /* UTrelloWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTrelloWidget.swift; sourceTree = ""; }; + A10000000000000000000032 /* UTrelloWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTrelloWidgetBundle.swift; sourceTree = ""; }; + A10000000000000000000033 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A10000000000000000000034 /* UTrelloWidget.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = UTrelloWidget.entitlements; sourceTree = ""; }; + A10000000000000000000040 /* UTrello.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UTrello.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A10000000000000000000041 /* UTrelloWidget.appex */ = {isa = PBXFileReference; explicitFileType = wrapper.app-extension; includeInIndex = 0; path = UTrelloWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A10000000000000000000091 /* Frameworks */ = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (A10000000000000000000005 /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + A10000000000000000000092 /* Frameworks */ = {isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = (A1000000000000000000000A /* WidgetKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A100000000000000000000A1 = {isa = PBXGroup; children = (A100000000000000000000A2 /* UTrello */, A100000000000000000000A5 /* UTrelloWidget */, A100000000000000000000A8 /* Frameworks */, A100000000000000000000A9 /* Products */, ); sourceTree = ""; }; + A100000000000000000000A2 /* UTrello */ = {isa = PBXGroup; path = UTrello; sourceTree = ""; children = (A100000000000000000000A3 /* App */, A100000000000000000000A4 /* Shared */, A10000000000000000000015 /* UTrello.entitlements */, ); }; + A100000000000000000000A3 /* App */ = {isa = PBXGroup; path = App; sourceTree = ""; children = (A10000000000000000000011 /* UTrelloApp.swift */, A10000000000000000000012 /* BoardView.swift */, ); }; + A100000000000000000000A4 /* Shared */ = {isa = PBXGroup; path = Shared; sourceTree = ""; children = (A10000000000000000000013 /* TaskItem.swift */, A10000000000000000000014 /* TaskStore.swift */, ); }; + A100000000000000000000A5 /* UTrelloWidget */ = {isa = PBXGroup; path = UTrelloWidget; sourceTree = ""; children = (A10000000000000000000031 /* UTrelloWidget.swift */, A10000000000000000000032 /* UTrelloWidgetBundle.swift */, A10000000000000000000033 /* Info.plist */, A10000000000000000000034 /* UTrelloWidget.entitlements */, ); }; + A100000000000000000000A8 /* Frameworks */ = {isa = PBXGroup; children = (A10000000000000000000021 /* WidgetKit.framework */, ); name = Frameworks; sourceTree = ""; }; + A100000000000000000000A9 /* Products */ = {isa = PBXGroup; children = (A10000000000000000000040 /* UTrello.app */, A10000000000000000000041 /* UTrelloWidget.appex */, ); name = Products; sourceTree = ""; }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A10000000000000000000070 /* UTrello */ = {isa = PBXNativeTarget; buildConfigurationList = A100000000000000000000B1 /* Build configuration list for PBXNativeTarget \"UTrello\" */; buildPhases = (A10000000000000000000093 /* Sources */, A10000000000000000000091 /* Frameworks */, A10000000000000000000081 /* Embed App Extensions */, ); buildRules = (); dependencies = (A100000000000000000000B2 /* PBXTargetDependency */, ); name = UTrello; productName = UTrello; productReference = A10000000000000000000040 /* UTrello.app */; productType = "com.apple.product-type.application"; }; + A10000000000000000000071 /* UTrelloWidget */ = {isa = PBXNativeTarget; buildConfigurationList = A100000000000000000000B3 /* Build configuration list for PBXNativeTarget \"UTrelloWidget\" */; buildPhases = (A10000000000000000000094 /* Sources */, A10000000000000000000092 /* Frameworks */, ); buildRules = (); dependencies = (); name = UTrelloWidget; productName = UTrelloWidget; productReference = A10000000000000000000041 /* UTrelloWidget.appex */; productType = "com.apple.product-type.app-extension"; }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A10000000000000000000061 /* Project object */ = {isa = PBXProject; attributes = {BuildIndependentTargetsInParallel = 1; LastUpgradeCheck = 1500; TargetAttributes = {A10000000000000000000071 = {CreatedOnToolsVersion = 15.0; }; }; }; buildConfigurationList = A100000000000000000000B0 /* Build configuration list for PBXProject \"UTrello\" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = (en, Base, ); mainGroup = A100000000000000000000A1; productRefGroup = A100000000000000000000A9 /* Products */; projectDirPath = ""; projectRoot = ""; targets = (A10000000000000000000070 /* UTrello */, A10000000000000000000071 /* UTrelloWidget */, ); }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A10000000000000000000093 /* Sources */ = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (A10000000000000000000001 /* UTrelloApp.swift in Sources */, A10000000000000000000002 /* BoardView.swift in Sources */, A10000000000000000000003 /* TaskItem.swift in Sources */, A10000000000000000000004 /* TaskStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + A10000000000000000000094 /* Sources */ = {isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = (A10000000000000000000006 /* UTrelloWidget.swift in Sources */, A10000000000000000000007 /* UTrelloWidgetBundle.swift in Sources */, A10000000000000000000008 /* TaskItem.swift in Sources */, A10000000000000000000009 /* TaskStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A100000000000000000000B2 /* PBXTargetDependency */ = {isa = PBXTargetDependency; target = A10000000000000000000071 /* UTrelloWidget */; targetProxy = A10000000000000000000051 /* PBXContainerItemProxy */; }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + A100000000000000000000C1 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {CLANG_ENABLE_MODULES = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; + A100000000000000000000C2 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {CLANG_ENABLE_MODULES = YES; SWIFT_VERSION = 5.0; }; name = Release; }; + A100000000000000000000C3 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {CODE_SIGN_ENTITLEMENTS = UTrello/UTrello.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = UTrello; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.UTrello; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Debug; }; + A100000000000000000000C4 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {CODE_SIGN_ENTITLEMENTS = UTrello/UTrello.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = UTrello; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.UTrello; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; name = Release; }; + A100000000000000000000C5 /* Debug */ = {isa = XCBuildConfiguration; buildSettings = {APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = UTrelloWidget/UTrelloWidget.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = UTrelloWidget/Info.plist; MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.UTrello.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; + A100000000000000000000C6 /* Release */ = {isa = XCBuildConfiguration; buildSettings = {APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = UTrelloWidget/UTrelloWidget.entitlements; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = UTrelloWidget/Info.plist; MACOSX_DEPLOYMENT_TARGET = 14.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.UTrello.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; }; name = Release; }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A100000000000000000000B0 /* Build configuration list for PBXProject \"UTrello\" */ = {isa = XCConfigurationList; buildConfigurations = (A100000000000000000000C1 /* Debug */, A100000000000000000000C2 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A100000000000000000000B1 /* Build configuration list for PBXNativeTarget \"UTrello\" */ = {isa = XCConfigurationList; buildConfigurations = (A100000000000000000000C3 /* Debug */, A100000000000000000000C4 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + A100000000000000000000B3 /* Build configuration list for PBXNativeTarget \"UTrelloWidget\" */ = {isa = XCConfigurationList; buildConfigurations = (A100000000000000000000C5 /* Debug */, A100000000000000000000C6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; +/* End XCConfigurationList section */ + }; + rootObject = A10000000000000000000061 /* Project object */; +} diff --git a/UTrello.xcodeproj/xcshareddata/xcschemes/UTrello.xcscheme b/UTrello.xcodeproj/xcshareddata/xcschemes/UTrello.xcscheme new file mode 100644 index 0000000..d9702e4 --- /dev/null +++ b/UTrello.xcodeproj/xcshareddata/xcschemes/UTrello.xcscheme @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/UTrello/App/BoardView.swift b/UTrello/App/BoardView.swift new file mode 100644 index 0000000..fe1bd3f --- /dev/null +++ b/UTrello/App/BoardView.swift @@ -0,0 +1,86 @@ +import SwiftUI + +struct BoardView: View { + @EnvironmentObject private var store: TaskStore + @State private var newTaskTitle = "" + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Text("My board") + .font(.largeTitle.bold()) + + HStack(spacing: 12) { + TextField("Add a task", text: $newTaskTitle) + .textFieldStyle(.roundedBorder) + .onSubmit(addTask) + Button("Add", action: addTask) + .keyboardShortcut(.defaultAction) + } + + HStack(alignment: .top, spacing: 16) { + ForEach(TaskList.allCases) { list in + TaskColumn(list: list, tasks: tasks(in: list)) + } + } + } + .padding(24) + .frame(minWidth: 780, minHeight: 460) + } + + private func tasks(in list: TaskList) -> [TaskItem] { + store.tasks.filter { $0.list == list } + } + + private func addTask() { + store.add(title: newTaskTitle) + newTaskTitle = "" + } +} + +private struct TaskColumn: View { + @EnvironmentObject private var store: TaskStore + let list: TaskList + let tasks: [TaskItem] + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(list.title) + .font(.headline) + ScrollView { + LazyVStack(spacing: 8) { + ForEach(tasks) { task in + TaskCard(task: task) + } + } + } + } + .padding(12) + .frame(maxWidth: .infinity, minHeight: 320, alignment: .topLeading) + .background(.quaternary, in: RoundedRectangle(cornerRadius: 12)) + } +} + +private struct TaskCard: View { + @EnvironmentObject private var store: TaskStore + let task: TaskItem + + var body: some View { + HStack(alignment: .top, spacing: 8) { + Text(task.title) + .frame(maxWidth: .infinity, alignment: .leading) + Menu { + ForEach(TaskList.allCases.filter { $0 != task.list }) { list in + Button("Move to \(list.title)") { store.move(task, to: list) } + } + Divider() + Button("Delete", role: .destructive) { store.delete(task) } + } label: { + Image(systemName: "ellipsis") + } + .menuStyle(.borderlessButton) + .fixedSize() + } + .padding(10) + .background(.background, in: RoundedRectangle(cornerRadius: 8)) + } +} diff --git a/UTrello/App/UTrelloApp.swift b/UTrello/App/UTrelloApp.swift new file mode 100644 index 0000000..dd5f705 --- /dev/null +++ b/UTrello/App/UTrelloApp.swift @@ -0,0 +1,14 @@ +import SwiftUI + +@main +struct UTrelloApp: App { + @StateObject private var store = TaskStore() + + var body: some Scene { + WindowGroup { + BoardView() + .environmentObject(store) + } + .windowResizability(.contentSize) + } +} diff --git a/UTrello/Shared/TaskItem.swift b/UTrello/Shared/TaskItem.swift new file mode 100644 index 0000000..109f755 --- /dev/null +++ b/UTrello/Shared/TaskItem.swift @@ -0,0 +1,31 @@ +import Foundation + +enum TaskList: String, CaseIterable, Codable, Identifiable { + case todo + case doing + case done + + var id: String { rawValue } + + var title: String { + switch self { + case .todo: "To do" + case .doing: "Doing" + case .done: "Done" + } + } +} + +struct TaskItem: Identifiable, Codable, Equatable { + let id: UUID + var title: String + var list: TaskList + let createdAt: Date + + init(id: UUID = UUID(), title: String, list: TaskList = .todo, createdAt: Date = .now) { + self.id = id + self.title = title + self.list = list + self.createdAt = createdAt + } +} diff --git a/UTrello/Shared/TaskStore.swift b/UTrello/Shared/TaskStore.swift new file mode 100644 index 0000000..646302f --- /dev/null +++ b/UTrello/Shared/TaskStore.swift @@ -0,0 +1,53 @@ +import Foundation +import Combine +#if canImport(WidgetKit) +import WidgetKit +#endif + +final class TaskStore: ObservableObject { + static let groupIdentifier = "group.com.example.utrello" + private static let tasksKey = "tasks" + + @Published private(set) var tasks: [TaskItem] { + didSet { save() } + } + + init() { + tasks = Self.loadTasks() + } + + func add(title: String, to list: TaskList = .todo) { + let title = title.trimmingCharacters(in: .whitespacesAndNewlines) + guard !title.isEmpty else { return } + tasks.insert(TaskItem(title: title, list: list), at: 0) + } + + func move(_ task: TaskItem, to list: TaskList) { + guard let index = tasks.firstIndex(of: task) else { return } + tasks[index].list = list + } + + func delete(_ task: TaskItem) { + tasks.removeAll { $0.id == task.id } + } + + private func save() { + guard let data = try? JSONEncoder().encode(tasks) else { return } + Self.defaults.set(data, forKey: Self.tasksKey) +#if canImport(WidgetKit) + WidgetCenter.shared.reloadTimelines(ofKind: "UTrelloTasksWidget") +#endif + } + + static func loadTasks() -> [TaskItem] { + guard + let data = defaults.data(forKey: tasksKey), + let tasks = try? JSONDecoder().decode([TaskItem].self, from: data) + else { return [] } + return tasks + } + + private static var defaults: UserDefaults { + UserDefaults(suiteName: groupIdentifier) ?? .standard + } +} diff --git a/UTrello/UTrello.entitlements b/UTrello/UTrello.entitlements new file mode 100644 index 0000000..8e441db --- /dev/null +++ b/UTrello/UTrello.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.example.utrello + + + diff --git a/UTrelloWidget/Info.plist b/UTrelloWidget/Info.plist new file mode 100644 index 0000000..87d4b16 --- /dev/null +++ b/UTrelloWidget/Info.plist @@ -0,0 +1,29 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + UTrello Widget + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/UTrelloWidget/UTrelloWidget.entitlements b/UTrelloWidget/UTrelloWidget.entitlements new file mode 100644 index 0000000..8e441db --- /dev/null +++ b/UTrelloWidget/UTrelloWidget.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.com.example.utrello + + + diff --git a/UTrelloWidget/UTrelloWidget.swift b/UTrelloWidget/UTrelloWidget.swift new file mode 100644 index 0000000..ca1e550 --- /dev/null +++ b/UTrelloWidget/UTrelloWidget.swift @@ -0,0 +1,67 @@ +import WidgetKit +import SwiftUI + +struct TaskEntry: TimelineEntry { + let date: Date + let tasks: [TaskItem] +} + +struct TaskProvider: TimelineProvider { + func placeholder(in context: Context) -> TaskEntry { + TaskEntry(date: .now, tasks: [TaskItem(title: "Plan the next task")]) + } + + func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) { + completion(TaskEntry(date: .now, tasks: openTasks())) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let entry = TaskEntry(date: .now, tasks: openTasks()) + let refresh = Calendar.current.date(byAdding: .minute, value: 15, to: .now)! + completion(Timeline(entries: [entry], policy: .after(refresh))) + } + + private func openTasks() -> [TaskItem] { + TaskStore.loadTasks().filter { $0.list != .done } + } +} + +struct UTrelloTasksWidget: Widget { + let kind = "UTrelloTasksWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: TaskProvider()) { entry in + TaskWidgetView(entry: entry) + } + .configurationDisplayName("UTrello Tasks") + .description("Your unfinished tasks at a glance.") + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +private struct TaskWidgetView: View { + let entry: TaskEntry + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text("My tasks") + .font(.headline) + if entry.tasks.isEmpty { + Spacer() + Text("All caught up") + .foregroundStyle(.secondary) + Spacer() + } else { + ForEach(entry.tasks.prefix(4)) { task in + Label(task.title, systemImage: task.list == .doing ? "play.circle" : "circle") + .lineLimit(1) + } + } + Spacer(minLength: 0) + Text("\(entry.tasks.count) open") + .font(.caption) + .foregroundStyle(.secondary) + } + .containerBackground(for: .widget) { Color.clear } + } +} diff --git a/UTrelloWidget/UTrelloWidgetBundle.swift b/UTrelloWidget/UTrelloWidgetBundle.swift new file mode 100644 index 0000000..cef6a01 --- /dev/null +++ b/UTrelloWidget/UTrelloWidgetBundle.swift @@ -0,0 +1,9 @@ +import WidgetKit +import SwiftUI + +@main +struct UTrelloWidgetBundle: WidgetBundle { + var body: some Widget { + UTrelloTasksWidget() + } +}