Matrix (KC-21): the Eisenhower Matrix is now a computed view of tasks. Importance = Priority (High = top row), urgency = deadline proximity, so tasks auto-place into Q1–Q4. Added a persisted `matrixOverride` so a manual drag pins the task and syncs its Priority to that row (top → High, moving down demotes High → Medium) and never gets forced back; editing Priority, due date, or the editor clears the override to re-place live. New tasks get KisaniCal priority defaults (birthday/domain/annual/exam/subscription → High, workout → Medium). AddTaskSheet drops the manual quadrant picker for a Priority menu (pick Priority + Date only). Also includes in-progress Progress Dots widget work (day/week/month/custom event modes, App-Group anchor, recurrence-aware spans). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
5.2 KiB
Swift
149 lines
5.2 KiB
Swift
import WidgetKit
|
|
import SwiftUI
|
|
import AppIntents
|
|
|
|
// MARK: - Interactive intents
|
|
|
|
/// Tapping a checkbox toggles the task's completion directly in the App Group.
|
|
struct ToggleTaskIntent: AppIntent {
|
|
static var title: LocalizedStringResource = "Toggle Task Complete"
|
|
|
|
@Parameter(title: "Task ID")
|
|
var taskId: String
|
|
|
|
init() {}
|
|
init(taskId: String) { self.taskId = taskId }
|
|
|
|
func perform() async throws -> some IntentResult {
|
|
toggleWidgetTaskComplete(id: taskId)
|
|
return .result()
|
|
}
|
|
}
|
|
|
|
/// The "+" button opens the app and asks it to present the add-task sheet.
|
|
struct AddTaskFromWidgetIntent: AppIntent {
|
|
static var title: LocalizedStringResource = "Add Task"
|
|
static var openAppWhenRun = true
|
|
|
|
func perform() async throws -> some IntentResult {
|
|
UserDefaults.kisani.set(true, forKey: "kisani.widget.openAddTask")
|
|
return .result()
|
|
}
|
|
}
|
|
|
|
// MARK: - Entry + Provider
|
|
|
|
struct TasksEntry: TimelineEntry {
|
|
let date: Date
|
|
let tasks: [WidgetTask]
|
|
let openCount: Int
|
|
}
|
|
|
|
struct TasksProvider: TimelineProvider {
|
|
func placeholder(in context: Context) -> TasksEntry {
|
|
TasksEntry(date: .now, tasks: [
|
|
WidgetTask(id: UUID(), title: "Pray for Uganda", dueDate: .now, isComplete: false,
|
|
quadrant: "Urgent + Important", category: "reminder"),
|
|
WidgetTask(id: UUID(), title: "Masters of the Universe", dueDate: .now, isComplete: false,
|
|
quadrant: "Schedule It", category: "reminder")
|
|
], openCount: 2)
|
|
}
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping (TasksEntry) -> Void) {
|
|
completion(loadEntry())
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<TasksEntry>) -> Void) {
|
|
let next = Calendar.current.startOfDay(for: Date().addingTimeInterval(86400))
|
|
completion(Timeline(entries: [loadEntry()], policy: .after(next)))
|
|
}
|
|
|
|
private func loadEntry() -> TasksEntry {
|
|
let tasks = todayWidgetTasks()
|
|
return TasksEntry(date: .now, tasks: tasks, openCount: tasks.filter { !$0.isComplete }.count)
|
|
}
|
|
}
|
|
|
|
// MARK: - Widget
|
|
|
|
struct MyTasksWidget: Widget {
|
|
let kind = "KisaniMyTasks"
|
|
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: kind, provider: TasksProvider()) { entry in
|
|
MyTasksView(entry: entry)
|
|
}
|
|
.configurationDisplayName("My Tasks")
|
|
.description("Today's tasks — check them off right from the widget.")
|
|
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
|
}
|
|
}
|
|
|
|
struct MyTasksView: View {
|
|
let entry: TasksEntry
|
|
@Environment(\.widgetFamily) var family
|
|
|
|
private var maxRows: Int {
|
|
switch family {
|
|
case .systemLarge: return 9
|
|
case .systemMedium: return 4
|
|
default: return 3
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(spacing: 6) {
|
|
Text("Today")
|
|
.font(.system(.body, design: .monospaced))
|
|
.foregroundColor(WidgetTheme.accent)
|
|
Text("\(entry.openCount)")
|
|
.font(.system(.body, design: .monospaced))
|
|
.foregroundColor(Color(white: 0.5))
|
|
Spacer()
|
|
Button(intent: AddTaskFromWidgetIntent()) {
|
|
Image(systemName: "plus")
|
|
.font(.system(size: 16))
|
|
.foregroundColor(WidgetTheme.accent)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
if entry.tasks.isEmpty {
|
|
Spacer()
|
|
Text("All clear for today 🎉")
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(Color(white: 0.5))
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
Spacer()
|
|
} else {
|
|
ForEach(entry.tasks.prefix(maxRows)) { task in
|
|
HStack(spacing: 10) {
|
|
Button(intent: ToggleTaskIntent(taskId: task.id.uuidString)) {
|
|
Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
|
|
.font(.system(size: 18))
|
|
.foregroundColor(task.isComplete ? task.accentColor : Color(white: 0.45))
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
Text(task.title)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundColor(task.isComplete ? Color(white: 0.45) : .white)
|
|
.strikethrough(task.isComplete, color: Color(white: 0.45))
|
|
.lineLimit(1)
|
|
Spacer(minLength: 0)
|
|
}
|
|
}
|
|
if entry.tasks.count > maxRows {
|
|
Text("+\(entry.tasks.count - maxRows) more")
|
|
.font(.system(.caption2, design: .monospaced))
|
|
.foregroundColor(Color(white: 0.4))
|
|
.padding(.leading, 28)
|
|
}
|
|
Spacer(minLength: 0)
|
|
}
|
|
}
|
|
.containerBackground(WidgetTheme.background, for: .widget)
|
|
}
|
|
}
|