Matrix: TickTick-style Priority×deadline view + Progress Dots widget (KC-21)
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>
This commit is contained in:
@@ -644,3 +644,41 @@ Files: `TaskContextMenu.swift`, `WidgetData.swift`, `EventCountdownWidget.swift`
|
||||
3. A yearly birthday shows Mode B: bar spans last year's → this year's date.
|
||||
4. Long-press the task again → "Stop Tracking Countdown" → widget falls back
|
||||
to its configured mode.
|
||||
|
||||
---
|
||||
|
||||
## KC-21 — Eisenhower Matrix as a TickTick-style view (Priority × deadline)
|
||||
|
||||
### Problem
|
||||
The Matrix treated importance as the task's *stored quadrant* — you placed a
|
||||
task in a row and only a manual "Move" changed it. TickTick instead treats the
|
||||
Matrix as a **view**: it auto-places every task from its **Priority** and **due
|
||||
date**, lets you drag freely between all four quadrants, and never forces a
|
||||
dragged task back.
|
||||
|
||||
### Fix (model — `TaskItem.swift`)
|
||||
- Added `matrixOverride: Quadrant?` (persisted, optional → old saves decode).
|
||||
- Importance is now `priority == .high` (top row), not the stored quadrant.
|
||||
Urgency stays deadline-driven (due today/overdue/≤ `urgentWindowDays`).
|
||||
Auto-placement: Q1 = High+urgent, Q2 = High+later, Q3 = not-High+urgent,
|
||||
Q4 = the rest. `displayQuadrant` returns `matrixOverride` if set, else auto.
|
||||
- `moveToQuadrant` (drag/menu) now pins `matrixOverride` **and** syncs Priority
|
||||
to the target row (top → High; moving down demotes High → Medium). The move
|
||||
sticks — nothing forces it back.
|
||||
- Editing Priority (`setPriority`), due date (`setDate`), or saving the editor
|
||||
(`updateTask`) clears `matrixOverride` so the task re-places live.
|
||||
- `addTask` applies KisaniCal defaults when no priority is picked: birthday /
|
||||
domain / annual (+ title keywords exam, subscription, renew, expir) → High;
|
||||
workout / gym / exercise → Medium.
|
||||
|
||||
### Fix (create UI — `TodayView.swift`)
|
||||
- `AddTaskSheet` no longer has a manual **quadrant** selector. It now exposes a
|
||||
**Priority** menu (flag icon). User picks Priority + Date only; placement is
|
||||
computed. Adding from a Matrix quadrant seeds High for the top row.
|
||||
|
||||
### How to test
|
||||
1. New task, no priority, far date → lands in Q4; raise to High → jumps to Q2.
|
||||
2. Drag it to Q1 → stays in Q1 (priority becomes High, override set).
|
||||
3. Drag a Q1 task to Q4 → High demotes to Medium, sticks in Q4.
|
||||
4. Open the task, change its due date sooner → override clears, re-places by rule.
|
||||
5. Add a "Mum's birthday" with no priority → auto-High.
|
||||
|
||||
@@ -23,6 +23,9 @@ struct TaskItem: Identifiable, Codable, Equatable {
|
||||
// Recurrence: optional so existing saved tasks decode safely (missing key → nil).
|
||||
var recurrenceEnd: Date? = nil // "repeat until" (nil = open-ended)
|
||||
var completedOccurrences: [String]? = nil // "yyyy-MM-dd" of completed occurrences
|
||||
// Matrix: a manual drag pins the task to a quadrant (overrides auto-placement).
|
||||
// nil = auto-place from Priority × deadline. Optional so old saves decode safely.
|
||||
var matrixOverride: Quadrant? = nil
|
||||
}
|
||||
|
||||
enum Priority: String, Codable, CaseIterable, Identifiable {
|
||||
@@ -332,18 +335,21 @@ class TaskViewModel: ObservableObject {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Eisenhower Matrix (classic: you set importance, time sets urgency)
|
||||
// MARK: - Eisenhower Matrix (TickTick-style: Priority × deadline, with manual override)
|
||||
//
|
||||
// Importance is intrinsic — taken from the task's assigned quadrant's row
|
||||
// (Q1/Q2 = important, Q3/Q4 = unimportant) and only changes when you move it.
|
||||
// Urgency is derived from the deadline: a task is urgent when its (next active)
|
||||
// due date is within `urgentWindowDays`. The displayed quadrant = importance × urgency,
|
||||
// so important tasks slide Q2→Q1 as they near, unimportant slide Q4→Q3, and
|
||||
// recurring tasks roll forward (a completed occurrence is replaced by the next).
|
||||
// The Matrix is a VIEW of tasks, not a separate system. A task is auto-placed:
|
||||
// • Importance (top row) = Priority is High; Medium/Low/None = bottom row.
|
||||
// • Urgency (left column) = its (next active) due date is today/overdue/within
|
||||
// `urgentWindowDays`; undated or far-out = right column.
|
||||
// So Q1 = High + urgent, Q2 = High + not urgent, Q3 = not-High + urgent, Q4 = the rest.
|
||||
// A manual drag (`moveToQuadrant`) pins the task via `matrixOverride` and syncs its
|
||||
// Priority to the target row — the move sticks and is never forced back. Editing the
|
||||
// Priority or due date in the task view clears the override so it re-places live.
|
||||
// Recurring tasks roll forward (a completed occurrence is replaced by the next).
|
||||
|
||||
static let urgentWindowDays = 7
|
||||
|
||||
private func isImportantQuadrant(_ q: Quadrant) -> Bool { q == .urgent || q == .schedule }
|
||||
private func isImportant(_ t: TaskItem) -> Bool { t.priority == .high }
|
||||
|
||||
/// The due date that drives urgency now: a recurring task's next active
|
||||
/// occurrence, or a one-off task's own due date.
|
||||
@@ -359,9 +365,9 @@ class TaskViewModel: ObservableObject {
|
||||
return days <= Self.urgentWindowDays // due soon / overdue
|
||||
}
|
||||
|
||||
/// Where a task should appear in the Matrix right now (importance × urgency).
|
||||
func displayQuadrant(_ t: TaskItem, on today: Date = Date()) -> Quadrant {
|
||||
switch (isImportantQuadrant(t.quadrant), isUrgent(t, on: today)) {
|
||||
/// Auto-placement from Priority (importance) × deadline (urgency).
|
||||
private func autoQuadrant(_ t: TaskItem, on today: Date) -> Quadrant {
|
||||
switch (isImportant(t), isUrgent(t, on: today)) {
|
||||
case (true, true): return .urgent
|
||||
case (true, false): return .schedule
|
||||
case (false, true): return .delegate_
|
||||
@@ -369,6 +375,11 @@ class TaskViewModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a task appears in the Matrix now: a manual override wins, else auto-placement.
|
||||
func displayQuadrant(_ t: TaskItem, on today: Date = Date()) -> Quadrant {
|
||||
t.matrixOverride ?? autoQuadrant(t, on: today)
|
||||
}
|
||||
|
||||
/// Matrix rows for a quadrant, grouped by the *displayed* (computed) quadrant.
|
||||
/// Recurring tasks contribute their single next-active occurrence.
|
||||
func matrixTasks(for quadrant: Quadrant, on today: Date = Date()) -> [TaskItem] {
|
||||
@@ -409,6 +420,7 @@ class TaskViewModel: ObservableObject {
|
||||
func setDate(_ task: TaskItem, date: Date?) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
tasks[idx].dueDate = date
|
||||
tasks[idx].matrixOverride = nil // deadline changed → re-place from urgency
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -421,14 +433,40 @@ class TaskViewModel: ObservableObject {
|
||||
func setPriority(_ task: TaskItem, priority: Priority) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||
tasks[idx].priority = priority
|
||||
tasks[idx].matrixOverride = nil // priority changed → re-place from importance
|
||||
save()
|
||||
}
|
||||
|
||||
/// Manual drag/move in the Matrix. Pins the task to the target quadrant and syncs
|
||||
/// its Priority to that row — top row (Q1/Q2) becomes High; moving down out of the
|
||||
/// top demotes High→Medium so it reads as unimportant. TickTick-style: it sticks.
|
||||
func moveToQuadrant(taskID: UUID, quadrant: Quadrant) {
|
||||
guard let idx = tasks.firstIndex(where: { $0.id == taskID }) else { return }
|
||||
tasks[idx].quadrant = quadrant
|
||||
tasks[idx].matrixOverride = quadrant
|
||||
tasks[idx].quadrant = quadrant // keep stored quadrant in sync (widget accent)
|
||||
let importantRow = (quadrant == .urgent || quadrant == .schedule)
|
||||
if importantRow {
|
||||
tasks[idx].priority = .high
|
||||
} else if tasks[idx].priority == .high {
|
||||
tasks[idx].priority = .medium
|
||||
}
|
||||
save()
|
||||
}
|
||||
|
||||
/// KisaniCal priority defaults — high-stakes events start High, workouts Medium.
|
||||
/// Applied only when the user didn't pick a priority (== .none) at creation.
|
||||
private func defaultPriority(category: TaskCategory, title: String) -> Priority {
|
||||
switch category {
|
||||
case .birthday, .domain, .annual: return .high
|
||||
default: break
|
||||
}
|
||||
let t = title.lowercased()
|
||||
if t.contains("birthday") || t.contains("exam") || t.contains("subscription")
|
||||
|| t.contains("renew") || t.contains("domain") || t.contains("expir") { return .high }
|
||||
if t.contains("workout") || t.contains("gym") || t.contains("exercise") { return .medium }
|
||||
return .none
|
||||
}
|
||||
|
||||
func addTask(title: String, dueDate: Date?, hasTime: Bool = false,
|
||||
quadrant: Quadrant = .urgent, category: TaskCategory = .reminder,
|
||||
taskColor: TaskColor = .orange, isRecurring: Bool = false,
|
||||
@@ -436,12 +474,14 @@ class TaskViewModel: ObservableObject {
|
||||
endDate: Date? = nil, isAllDay: Bool = false,
|
||||
priority: Priority = .none, reminderDate: Date? = nil,
|
||||
constantReminder: Bool = false, recurrenceEnd: Date? = nil) {
|
||||
let resolvedPriority = priority == .none
|
||||
? defaultPriority(category: category, title: title) : priority
|
||||
var item = TaskItem(title: title, dueDate: dueDate, hasTime: hasTime,
|
||||
quadrant: quadrant, category: category,
|
||||
taskColor: taskColor, isRecurring: isRecurring,
|
||||
recurrenceLabel: recurrenceLabel,
|
||||
endDate: endDate, isAllDay: isAllDay,
|
||||
priority: priority, reminderDate: reminderDate,
|
||||
priority: resolvedPriority, reminderDate: reminderDate,
|
||||
constantReminder: constantReminder)
|
||||
item.recurrenceEnd = recurrenceEnd
|
||||
tasks.append(item)
|
||||
@@ -463,6 +503,7 @@ class TaskViewModel: ObservableObject {
|
||||
tasks[idx].reminderDate = reminderDate
|
||||
tasks[idx].constantReminder = constantReminder
|
||||
tasks[idx].recurrenceEnd = recurrenceEnd
|
||||
tasks[idx].matrixOverride = nil // editor saved a new deadline → re-place
|
||||
save()
|
||||
}
|
||||
|
||||
@@ -488,4 +529,3 @@ class TaskViewModel: ObservableObject {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,20 +47,23 @@ struct TaskMenuItems: View {
|
||||
}
|
||||
|
||||
Menu {
|
||||
Button { onSetDate(task, cal.startOfDay(for: Date())) } label: {
|
||||
Label("Today", systemImage: "sun.max")
|
||||
Button { onSetDate(task, preservingTime(on: Date())) } label: {
|
||||
Label("Today", systemImage: "calendar")
|
||||
}
|
||||
Button { onSetDate(task, preservingTime(on: dayOffset(1))) } label: {
|
||||
Label("Tomorrow", systemImage: "sun.max")
|
||||
}
|
||||
Button { onSetDate(task, preservingTime(on: nextMonday())) } label: {
|
||||
Label("Next Monday", systemImage: "calendar.badge.clock")
|
||||
}
|
||||
Button {
|
||||
let d = cal.date(byAdding: .day, value: 1, to: Date())!
|
||||
onSetDate(task, cal.startOfDay(for: d))
|
||||
} label: { Label("Tomorrow", systemImage: "sunrise") }
|
||||
Button {
|
||||
let d = cal.date(byAdding: .weekOfYear, value: 1, to: Date())!
|
||||
onSetDate(task, cal.startOfDay(for: d))
|
||||
} label: { Label("Next Week", systemImage: "calendar") }
|
||||
Divider()
|
||||
if let onEdit {
|
||||
Button { onEdit(task) } label: {
|
||||
Label("Pick Date", systemImage: "calendar.badge.plus")
|
||||
}
|
||||
}
|
||||
Button { onSetDate(task, nil) } label: {
|
||||
Label("Remove Date", systemImage: "xmark.circle")
|
||||
Label("Clear", systemImage: "xmark.square")
|
||||
}
|
||||
} label: { Label("Date", systemImage: "calendar.badge.clock") }
|
||||
|
||||
@@ -87,7 +90,7 @@ struct TaskMenuItems: View {
|
||||
} label: { Label("Tags", systemImage: "tag") }
|
||||
|
||||
Button { onLiveActivity(task) } label: {
|
||||
Label("Add to Live Activity", systemImage: "pin.circle")
|
||||
Label(liveActivityTitle, systemImage: liveActivityIcon)
|
||||
}
|
||||
|
||||
Button { CountdownTracker.toggle(task) } label: {
|
||||
@@ -105,4 +108,36 @@ struct TaskMenuItems: View {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
private var liveActivityTitle: String {
|
||||
if #available(iOS 16.1, *), LiveActivityManager.isActive(taskID: task.id.uuidString) {
|
||||
return "Remove from Live Activity"
|
||||
}
|
||||
return "Add to Live Activity"
|
||||
}
|
||||
|
||||
private var liveActivityIcon: String {
|
||||
if #available(iOS 16.1, *), LiveActivityManager.isActive(taskID: task.id.uuidString) {
|
||||
return "pin.slash"
|
||||
}
|
||||
return "pin.circle"
|
||||
}
|
||||
|
||||
private func dayOffset(_ value: Int) -> Date {
|
||||
cal.date(byAdding: .day, value: value, to: Date()) ?? Date()
|
||||
}
|
||||
|
||||
private func nextMonday() -> Date {
|
||||
var comps = DateComponents()
|
||||
comps.weekday = 2
|
||||
return cal.nextDate(after: Date(), matching: comps, matchingPolicy: .nextTimePreservingSmallerComponents)
|
||||
?? dayOffset(7)
|
||||
}
|
||||
|
||||
private func preservingTime(on day: Date) -> Date {
|
||||
let base = cal.startOfDay(for: day)
|
||||
guard task.hasTime, let due = task.dueDate else { return base }
|
||||
let parts = cal.dateComponents([.hour, .minute], from: due)
|
||||
return cal.date(bySettingHour: parts.hour ?? 0, minute: parts.minute ?? 0, second: 0, of: base) ?? base
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,11 @@ struct TodayView: View {
|
||||
onPostponeTask: { taskVM.postpone($0) },
|
||||
onPin: { taskVM.pin($0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
||||
)
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.top, 10)
|
||||
@@ -139,7 +143,13 @@ struct TodayView: View {
|
||||
tasks: taskVM.tomorrowTasks,
|
||||
onToggle: { taskVM.toggle($0) },
|
||||
onPostpone: { taskVM.postpone($0) },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
onPin: { taskVM.pin($0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,7 +160,13 @@ struct TodayView: View {
|
||||
tasks: taskVM.next7DaysTasks,
|
||||
onToggle: { taskVM.toggle($0) },
|
||||
onPostpone: { taskVM.postpone($0) },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
onPin: { taskVM.pin($0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -161,7 +177,13 @@ struct TodayView: View {
|
||||
tasks: taskVM.laterTasks,
|
||||
onToggle: { taskVM.toggle($0) },
|
||||
onPostpone: { taskVM.postpone($0) },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
onPin: { taskVM.pin($0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -512,6 +534,10 @@ struct OverdueCard: View {
|
||||
var onPin: (TaskItem) -> Void = { _ in }
|
||||
var onEdit: (TaskItem) -> Void = { _ in }
|
||||
var onDelete: (TaskItem) -> Void = { _ in }
|
||||
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||
|
||||
@State private var expanded = true
|
||||
|
||||
@@ -599,25 +625,24 @@ struct OverdueCard: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func taskMenu(_ task: TaskItem) -> some View {
|
||||
Button { withAnimation { onToggle(task) } } label: {
|
||||
Button { withAnimation(KisaniSpring.bounce) { onToggle(task) } } label: {
|
||||
Label("Mark Complete", systemImage: "checkmark.circle")
|
||||
}
|
||||
Button { onPostponeTask(task) } label: {
|
||||
Button { withAnimation(KisaniSpring.snappy) { onPostponeTask(task) } } label: {
|
||||
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
||||
}
|
||||
Button { onPin(task) } label: {
|
||||
Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin")
|
||||
}
|
||||
Button { onEdit(task) } label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
ShareLink(item: task.title) {
|
||||
Label("Share", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive) { onDelete(task) } label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
onPin: onPin,
|
||||
onSetDate: onSetDate,
|
||||
onMove: onMove,
|
||||
onSetPriority: onSetPriority,
|
||||
onSetCategory: onSetCategory,
|
||||
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,7 +944,13 @@ struct UpcomingSection: View {
|
||||
let tasks: [TaskItem]
|
||||
let onToggle: (TaskItem) -> Void
|
||||
let onPostpone: (TaskItem) -> Void
|
||||
var onPin: (TaskItem) -> Void = { _ in }
|
||||
var onEdit: (TaskItem) -> Void = { _ in }
|
||||
let onDelete: (TaskItem) -> Void
|
||||
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||
|
||||
@State private var collapsed = false
|
||||
@State private var showAll = false
|
||||
@@ -972,13 +1003,21 @@ struct UpcomingSection: View {
|
||||
withAnimation(KisaniSpring.snappy) { onToggle(task) }
|
||||
}
|
||||
.contextMenu {
|
||||
Button { withAnimation { onPostpone(task) } } label: {
|
||||
Button { withAnimation(KisaniSpring.snappy) { onPostpone(task) } } label: {
|
||||
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
||||
}
|
||||
Divider()
|
||||
Button(role: .destructive) { withAnimation { onDelete(task) } } label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
onPin: onPin,
|
||||
onSetDate: onSetDate,
|
||||
onMove: onMove,
|
||||
onSetPriority: onSetPriority,
|
||||
onSetCategory: onSetCategory,
|
||||
onLiveActivity: { LiveActivityManager.toggle(for: $0) },
|
||||
onEdit: onEdit,
|
||||
onDelete: { task in withAnimation(KisaniSpring.snappy) { onDelete(task) } }
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 18)
|
||||
.padding(.bottom, 5)
|
||||
@@ -1682,7 +1721,7 @@ struct AddTaskSheet: View {
|
||||
|
||||
@State private var rawText = ""
|
||||
@State private var parsed = NLParsed()
|
||||
@State private var selectedQuadrant: Quadrant
|
||||
@State private var selectedPriority: Priority = .none
|
||||
@State private var selectedCategory: TaskCategory = .reminder
|
||||
@State private var reminderDate: Date? = nil
|
||||
@State private var constantReminder = false
|
||||
@@ -1694,7 +1733,9 @@ struct AddTaskSheet: View {
|
||||
init(prefilledDate: Date? = nil, prefilledQuadrant: Quadrant = .urgent) {
|
||||
self.prefilledDate = prefilledDate
|
||||
self.prefilledQuadrant = prefilledQuadrant
|
||||
_selectedQuadrant = State(initialValue: prefilledQuadrant)
|
||||
// Adding from a Matrix quadrant: seed the Priority that lands the task there.
|
||||
let importantRow = (prefilledQuadrant == .urgent || prefilledQuadrant == .schedule)
|
||||
_selectedPriority = State(initialValue: importantRow ? .high : .none)
|
||||
var p = NLParsed()
|
||||
p.date = prefilledDate ?? Calendar.current.startOfDay(for: Date())
|
||||
_parsed = State(initialValue: p)
|
||||
@@ -1762,27 +1803,18 @@ struct AddTaskSheet: View {
|
||||
|
||||
Spacer(minLength: 4)
|
||||
|
||||
// Quadrant
|
||||
// Priority — drives Matrix placement (importance axis), TickTick-style
|
||||
Menu {
|
||||
ForEach(Quadrant.allCases) { q in
|
||||
Button { withAnimation(KisaniSpring.micro) { selectedQuadrant = q } } label: {
|
||||
Label {
|
||||
Text(q.matrixLabel)
|
||||
} icon: {
|
||||
Image(uiImage: quadrantBadgeImage(q))
|
||||
}
|
||||
ForEach(Priority.allCases) { p in
|
||||
Button { withAnimation(KisaniSpring.micro) { selectedPriority = p } } label: {
|
||||
Label(p.label, systemImage: selectedPriority == p ? "checkmark" : "flag")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: 3) {
|
||||
Text(selectedQuadrant.roman)
|
||||
.font(AppFonts.mono(8, weight: .heavy))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 14, height: 14)
|
||||
.background(selectedQuadrant.color)
|
||||
.clipShape(Circle())
|
||||
}
|
||||
.frame(width: 42, height: 44)
|
||||
Image(systemName: selectedPriority == .none ? "flag" : "flag.fill")
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(selectedPriority == .none ? AppColors.text3(cs) : selectedPriority.color)
|
||||
.frame(width: 42, height: 44)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
@@ -1858,35 +1890,14 @@ struct AddTaskSheet: View {
|
||||
private func submit() {
|
||||
guard canAdd else { return }
|
||||
taskVM.addTask(title: cleanTitle, dueDate: parsed.date, hasTime: parsed.hasTime,
|
||||
quadrant: selectedQuadrant, category: selectedCategory,
|
||||
quadrant: prefilledQuadrant, category: selectedCategory,
|
||||
isRecurring: parsed.isRecurring, recurrenceLabel: parsed.recurrenceLabel,
|
||||
endDate: parsed.endDate, isAllDay: parsed.isAllDay,
|
||||
priority: selectedPriority,
|
||||
reminderDate: reminderDate, constantReminder: constantReminder,
|
||||
recurrenceEnd: parsed.recurrenceEnd)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func quadrantBadgeImage(_ q: Quadrant) -> UIImage {
|
||||
let uiColor: UIColor
|
||||
switch q {
|
||||
case .urgent: uiColor = UIColor(AppColors.accent)
|
||||
case .schedule: uiColor = UIColor(AppColors.yellow)
|
||||
case .delegate_: uiColor = UIColor(AppColors.blue)
|
||||
case .eliminate: uiColor = UIColor(AppColors.green)
|
||||
}
|
||||
let size = CGSize(width: 20, height: 20)
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
let img = renderer.image { ctx in
|
||||
uiColor.setFill()
|
||||
UIBezierPath(ovalIn: CGRect(origin: .zero, size: size)).fill()
|
||||
let label = q.roman
|
||||
let font = UIFont.monospacedSystemFont(ofSize: 8, weight: .heavy)
|
||||
let attrs: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: UIColor.white]
|
||||
let ts = label.size(withAttributes: attrs)
|
||||
label.draw(at: CGPoint(x: (size.width - ts.width) / 2, y: (size.height - ts.height) / 2), withAttributes: attrs)
|
||||
}
|
||||
return img.withRenderingMode(.alwaysOriginal)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Task Date Picker Sheet
|
||||
|
||||
@@ -186,6 +186,7 @@ struct InViewTutorialCard: View {
|
||||
@Binding var step: Int
|
||||
let onAdvance: () -> Void
|
||||
let onSkip: () -> Void
|
||||
@Environment(\.colorScheme) private var cs
|
||||
|
||||
private var current: PageTip { tips[min(step, tips.count - 1)] }
|
||||
private var isLast: Bool { step == tips.count - 1 }
|
||||
@@ -203,17 +204,17 @@ struct InViewTutorialCard: View {
|
||||
}
|
||||
Text(current.title)
|
||||
.font(AppFonts.sans(15, weight: .bold))
|
||||
.foregroundColor(.white)
|
||||
.foregroundColor(AppColors.text(cs))
|
||||
Spacer()
|
||||
Button("Skip") { onSkip() }
|
||||
.font(AppFonts.sans(11))
|
||||
.foregroundColor(.white.opacity(0.45))
|
||||
.foregroundColor(AppColors.text3(cs))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Text(current.body)
|
||||
.font(AppFonts.sans(13))
|
||||
.foregroundColor(.white.opacity(0.80))
|
||||
.foregroundColor(AppColors.text2(cs))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineSpacing(2)
|
||||
|
||||
@@ -221,7 +222,7 @@ struct InViewTutorialCard: View {
|
||||
HStack(spacing: 5) {
|
||||
ForEach(0..<tips.count, id: \.self) { i in
|
||||
Capsule()
|
||||
.fill(i == step ? AppColors.accent : Color.white.opacity(0.20))
|
||||
.fill(i == step ? AppColors.accent : AppColors.text3(cs).opacity(0.22))
|
||||
.frame(width: i == step ? 18 : 6, height: 6)
|
||||
.animation(KisaniSpring.micro, value: step)
|
||||
}
|
||||
@@ -243,8 +244,9 @@ struct InViewTutorialCard: View {
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(Color.white.opacity(0.10), lineWidth: 1))
|
||||
.background(AppColors.surface(cs), in: RoundedRectangle(cornerRadius: 20))
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(AppColors.border(cs), lineWidth: 1))
|
||||
.shadow(color: Color.black.opacity(cs == .dark ? 0.28 : 0.08), radius: 18, x: 0, y: 10)
|
||||
.padding(.horizontal, 20)
|
||||
.id(step)
|
||||
.transition(.asymmetric(
|
||||
|
||||
Reference in New Issue
Block a user