feat: add macOS task board widget app
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.example.utrello</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user