Matrix/menus: Pick Date everywhere, time-based reminders, notification snooze (KC-21)
- Date menu now offers "Pick Date" (and Edit) in the Matrix grid cards and the Calendar reschedule/move menu, matching the tasks view — both open the shared TaskEditSheet via a new editingTask sheet. - Reworked the reminder picker (TaskDatePickerSheet, shared by all entry points) from day/week offsets to sensible time-based ones relative to the event time: None, On time, 5 min, 30 min, 1 hour, 1 day, plus a Minutes/Hours/Days custom wheel. Existing absolute reminderDates migrate to the nearest minute offset. - Notifications gain Snooze actions (15/30/60/120 min) that reschedule the task reminder; postpone now clears the Matrix urgency override. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -649,24 +649,36 @@ Files: `TaskContextMenu.swift`, `WidgetData.swift`, `EventCountdownWidget.swift`
|
|||||||
|
|
||||||
## KC-21 — Eisenhower Matrix as a TickTick-style view (Priority × deadline)
|
## KC-21 — Eisenhower Matrix as a TickTick-style view (Priority × deadline)
|
||||||
|
|
||||||
|
**Status:** In Progress (implemented & build-verified, pending device QA)
|
||||||
|
**Reported by:** User
|
||||||
|
**Area:** Tasks / Matrix
|
||||||
|
|
||||||
### Problem
|
### Problem
|
||||||
The Matrix treated importance as the task's *stored quadrant* — you placed a
|
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
|
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
|
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
|
date**, while still allowing quick drag/menu moves.
|
||||||
dragged task back.
|
|
||||||
|
|
||||||
### Fix (model — `TaskItem.swift`)
|
### Fix (model — `TaskItem.swift`)
|
||||||
- Added `matrixOverride: Quadrant?` (persisted, optional → old saves decode).
|
- Replaced whole-quadrant override with `urgencyOverride: Bool?`.
|
||||||
|
- `nil` = urgency is computed from due date.
|
||||||
|
- `true` = force urgent column.
|
||||||
|
- `false` = force not-urgent column.
|
||||||
- Importance is now `priority == .high` (top row), not the stored quadrant.
|
- Importance is now `priority == .high` (top row), not the stored quadrant.
|
||||||
Urgency stays deadline-driven (due today/overdue/≤ `urgentWindowDays`).
|
Medium/Low/None stay in the bottom row.
|
||||||
|
- Urgency is deadline-driven:
|
||||||
|
- default urgent window = 3 days
|
||||||
|
- birthday/domain/annual/renewal/subscription/exam/deadline = 14 days
|
||||||
Auto-placement: Q1 = High+urgent, Q2 = High+later, Q3 = not-High+urgent,
|
Auto-placement: Q1 = High+urgent, Q2 = High+later, Q3 = not-High+urgent,
|
||||||
Q4 = the rest. `displayQuadrant` returns `matrixOverride` if set, else auto.
|
Q4 = the rest.
|
||||||
- `moveToQuadrant` (drag/menu) now pins `matrixOverride` **and** syncs Priority
|
- `moveToQuadrant` updates real attributes:
|
||||||
to the target row (top → High; moving down demotes High → Medium). The move
|
- vertical movement sets Priority (top → High; moving down demotes High → Medium)
|
||||||
sticks — nothing forces it back.
|
- horizontal movement sets `urgencyOverride` only if the target column disagrees
|
||||||
- Editing Priority (`setPriority`), due date (`setDate`), or saving the editor
|
with date-derived urgency.
|
||||||
(`updateTask`) clears `matrixOverride` so the task re-places live.
|
- Editing due date (`setDate`) or saving the editor (`updateTask`) clears
|
||||||
|
`urgencyOverride` so the task re-places live.
|
||||||
|
- Category changes, priority changes, and date changes refresh the legacy
|
||||||
|
`quadrant` color/widget fallback to match computed placement.
|
||||||
- `addTask` applies KisaniCal defaults when no priority is picked: birthday /
|
- `addTask` applies KisaniCal defaults when no priority is picked: birthday /
|
||||||
domain / annual (+ title keywords exam, subscription, renew, expir) → High;
|
domain / annual (+ title keywords exam, subscription, renew, expir) → High;
|
||||||
workout / gym / exercise → Medium.
|
workout / gym / exercise → Medium.
|
||||||
@@ -675,10 +687,72 @@ dragged task back.
|
|||||||
- `AddTaskSheet` no longer has a manual **quadrant** selector. It now exposes a
|
- `AddTaskSheet` no longer has a manual **quadrant** selector. It now exposes a
|
||||||
**Priority** menu (flag icon). User picks Priority + Date only; placement is
|
**Priority** menu (flag icon). User picks Priority + Date only; placement is
|
||||||
computed. Adding from a Matrix quadrant seeds High for the top row.
|
computed. Adding from a Matrix quadrant seeds High for the top row.
|
||||||
|
- Fixed no-date creation: a task without an explicit parsed/picked date stays
|
||||||
|
"No Date" instead of silently becoming today/urgent.
|
||||||
|
|
||||||
|
### Fix (Matrix UI — `MatrixView.swift`, `TaskContextMenu.swift`)
|
||||||
|
- Completed tasks are hidden from the 2×2 Matrix grid by default, but remain in
|
||||||
|
the per-quadrant detail Completed section.
|
||||||
|
- Rows with an urgency override show a small **Manual** badge.
|
||||||
|
- Context menu includes **Reset Matrix Urgency** when an urgency override exists.
|
||||||
|
- Move menu shows all four quadrants, with a checkmark on the current quadrant.
|
||||||
|
|
||||||
### How to test
|
### How to test
|
||||||
1. New task, no priority, far date → lands in Q4; raise to High → jumps to Q2.
|
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).
|
2. New task with no date → stays in right column until given a due date/reminder.
|
||||||
3. Drag a Q1 task to Q4 → High demotes to Medium, sticks in Q4.
|
3. Drag Q2 → Q1 when due date is far out → Manual badge appears.
|
||||||
4. Open the task, change its due date sooner → override clears, re-places by rule.
|
4. Reset Matrix Urgency → task returns to its date-derived column.
|
||||||
5. Add a "Mum's birthday" with no priority → auto-High.
|
5. Drag Q1 → Q4 → High demotes to Medium and urgency is overridden if needed.
|
||||||
|
6. Open the task, change its due date → urgency override clears, re-places by rule.
|
||||||
|
7. Add a "Mum's birthday" with no priority → auto-High and uses 14-day urgency window.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## KC-22 — Quick postpone / snooze from task menus and lock screen
|
||||||
|
|
||||||
|
**Status:** In Progress (implemented & build-verified, pending device QA)
|
||||||
|
**Reported by:** User
|
||||||
|
**Area:** Tasks / Calendar / Matrix / Notifications
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Long-pressing a task/event in Today, Matrix, or Calendar required going into the
|
||||||
|
date editor or using only coarse "+1 day" actions. Lock-screen task reminders
|
||||||
|
also only supported "Mark Complete". The user wanted standard task-manager
|
||||||
|
snooze choices directly from the pressed item and notification actions.
|
||||||
|
|
||||||
|
### Fix (app task menus)
|
||||||
|
- Added a shared **Postpone** submenu to `TaskMenuItems`:
|
||||||
|
- 15 Minutes
|
||||||
|
- 30 Minutes
|
||||||
|
- 1 Hour
|
||||||
|
- 2 Hours
|
||||||
|
- Tomorrow
|
||||||
|
- Wired the submenu through:
|
||||||
|
- Today timeline + overdue/upcoming sections
|
||||||
|
- Matrix grid + quadrant detail sections
|
||||||
|
- Calendar day timeline
|
||||||
|
- `TaskViewModel.postpone(_:minutes:)` applies the nudge:
|
||||||
|
- existing reminder → reminder moves forward
|
||||||
|
- due date with no reminder → due date/time moves forward and becomes timed
|
||||||
|
- no date/reminder → creates a reminder
|
||||||
|
|
||||||
|
### Fix (lock screen notifications)
|
||||||
|
- Task reminder notifications now include actions:
|
||||||
|
- 15 min
|
||||||
|
- 30 min
|
||||||
|
- 1 hour
|
||||||
|
- 2 hours
|
||||||
|
- Notification response handler updates the stored task reminder and schedules
|
||||||
|
a one-off snoozed notification for the selected delay.
|
||||||
|
|
||||||
|
Files: `TaskContextMenu.swift`, `TaskItem.swift`, `TodayView.swift`,
|
||||||
|
`MatrixView.swift`, `CalendarView.swift`, `NotificationManager.swift`.
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
1. Long-press a task in Today → Postpone → 15 Minutes; reminder/due time moves.
|
||||||
|
2. Repeat from Matrix grid, Matrix detail, and Calendar day timeline.
|
||||||
|
3. Open Move from the same menu → all four quadrants are visible, current one checked.
|
||||||
|
4. Trigger a task reminder notification on device → verify 15/30/60/120 minute
|
||||||
|
lock-screen actions appear.
|
||||||
|
5. Tap a lock-screen snooze action → task reminder updates and notification
|
||||||
|
re-delivers after the selected interval.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
|
|
||||||
private static let categoryId = "TASK_REMINDER"
|
private static let categoryId = "TASK_REMINDER"
|
||||||
private static let actionMarkId = "MARK_COMPLETE"
|
private static let actionMarkId = "MARK_COMPLETE"
|
||||||
|
private static let taskSnoozePrefix = "TASK_SNOOZE_"
|
||||||
private static let confirmCatId = "WORKOUT_CONFIRM"
|
private static let confirmCatId = "WORKOUT_CONFIRM"
|
||||||
private static let logWorkoutActId = "LOG_WORKOUT"
|
private static let logWorkoutActId = "LOG_WORKOUT"
|
||||||
private static let missedWorkoutActId = "MISSED_WORKOUT"
|
private static let missedWorkoutActId = "MISSED_WORKOUT"
|
||||||
@@ -28,7 +29,13 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
|
|
||||||
private func registerCategories() {
|
private func registerCategories() {
|
||||||
let markAction = UNNotificationAction(identifier: Self.actionMarkId, title: "Mark Complete", options: [])
|
let markAction = UNNotificationAction(identifier: Self.actionMarkId, title: "Mark Complete", options: [])
|
||||||
let taskCat = UNNotificationCategory(identifier: Self.categoryId, actions: [markAction],
|
let taskSnoozeActions = [
|
||||||
|
UNNotificationAction(identifier: Self.taskSnoozePrefix + "15", title: "15 min", options: []),
|
||||||
|
UNNotificationAction(identifier: Self.taskSnoozePrefix + "30", title: "30 min", options: []),
|
||||||
|
UNNotificationAction(identifier: Self.taskSnoozePrefix + "60", title: "1 hour", options: []),
|
||||||
|
UNNotificationAction(identifier: Self.taskSnoozePrefix + "120", title: "2 hours", options: [])
|
||||||
|
]
|
||||||
|
let taskCat = UNNotificationCategory(identifier: Self.categoryId, actions: [markAction] + taskSnoozeActions,
|
||||||
intentIdentifiers: [], options: .customDismissAction)
|
intentIdentifiers: [], options: .customDismissAction)
|
||||||
|
|
||||||
let logAction = UNNotificationAction(identifier: Self.logWorkoutActId,
|
let logAction = UNNotificationAction(identifier: Self.logWorkoutActId,
|
||||||
@@ -59,6 +66,29 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func snoozeTask(taskId: String, userId: String, minutes: Int, content: UNNotificationContent) {
|
||||||
|
let key = "kisani.tasks.v2.\(userId)"
|
||||||
|
let fireDate = Date().addingTimeInterval(TimeInterval(minutes * 60))
|
||||||
|
if let data = UserDefaults.kisani.data(forKey: key),
|
||||||
|
var tasks = try? JSONDecoder().decode([TaskItem].self, from: data),
|
||||||
|
let uuid = UUID(uuidString: taskId),
|
||||||
|
let idx = tasks.firstIndex(where: { $0.id == uuid }) {
|
||||||
|
tasks[idx].reminderDate = fireDate
|
||||||
|
if let encoded = try? JSONEncoder().encode(tasks) {
|
||||||
|
UserDefaults.kisani.set(encoded, forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mutable = content.mutableCopy() as! UNMutableNotificationContent
|
||||||
|
mutable.categoryIdentifier = Self.categoryId
|
||||||
|
mutable.userInfo = content.userInfo
|
||||||
|
center.add(UNNotificationRequest(
|
||||||
|
identifier: "kisani.task.snooze.\(taskId).\(minutes).\(UUID().uuidString)",
|
||||||
|
content: mutable,
|
||||||
|
trigger: UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(minutes * 60), repeats: false)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Permission
|
// MARK: - Permission
|
||||||
|
|
||||||
func requestPermission() {
|
func requestPermission() {
|
||||||
@@ -425,6 +455,13 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
|
|||||||
case Self.actionMarkId:
|
case Self.actionMarkId:
|
||||||
if let taskId { markTaskComplete(taskId: taskId, userId: userId) }
|
if let taskId { markTaskComplete(taskId: taskId, userId: userId) }
|
||||||
|
|
||||||
|
case let action where action.hasPrefix(Self.taskSnoozePrefix):
|
||||||
|
if let taskId,
|
||||||
|
let minutes = Int(action.replacingOccurrences(of: Self.taskSnoozePrefix, with: "")) {
|
||||||
|
snoozeTask(taskId: taskId, userId: userId, minutes: minutes,
|
||||||
|
content: response.notification.request.content)
|
||||||
|
}
|
||||||
|
|
||||||
case Self.logWorkoutActId:
|
case Self.logWorkoutActId:
|
||||||
// "Yes, log it" from workout confirm notification — store date for WorkoutViewModel to pick up on next foreground
|
// "Yes, log it" from workout confirm notification — store date for WorkoutViewModel to pick up on next foreground
|
||||||
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
|
let fmt = DateFormatter(); fmt.dateFormat = "yyyy-MM-dd"
|
||||||
|
|||||||
@@ -562,6 +562,24 @@ class TaskViewModel: ObservableObject {
|
|||||||
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
let base = tasks[idx].dueDate ?? Date()
|
let base = tasks[idx].dueDate ?? Date()
|
||||||
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
|
tasks[idx].dueDate = Calendar.current.date(byAdding: .day, value: days, to: base)
|
||||||
|
tasks[idx].urgencyOverride = nil
|
||||||
|
tasks[idx].quadrant = displayQuadrant(tasks[idx])
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func postpone(_ task: TaskItem, minutes: Int) {
|
||||||
|
guard let idx = tasks.firstIndex(where: { $0.id == task.id }) else { return }
|
||||||
|
let target = Date().addingTimeInterval(TimeInterval(minutes * 60))
|
||||||
|
if tasks[idx].reminderDate != nil {
|
||||||
|
tasks[idx].reminderDate = target
|
||||||
|
} else if tasks[idx].dueDate != nil {
|
||||||
|
tasks[idx].dueDate = target
|
||||||
|
tasks[idx].hasTime = true
|
||||||
|
tasks[idx].urgencyOverride = nil
|
||||||
|
tasks[idx].quadrant = displayQuadrant(tasks[idx])
|
||||||
|
} else {
|
||||||
|
tasks[idx].reminderDate = target
|
||||||
|
}
|
||||||
save()
|
save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ struct CalendarView: View {
|
|||||||
@State private var showCalendarConnect = false
|
@State private var showCalendarConnect = false
|
||||||
@State private var showWorkoutSession = false
|
@State private var showWorkoutSession = false
|
||||||
@State private var editingEvent: EditableEvent? = nil
|
@State private var editingEvent: EditableEvent? = nil
|
||||||
|
@State private var editingTask: TaskItem? = nil
|
||||||
|
|
||||||
private let cal = Calendar.current
|
private let cal = Calendar.current
|
||||||
private let weekdays = ["S","M","T","W","T","F","S"]
|
private let weekdays = ["S","M","T","W","T","F","S"]
|
||||||
@@ -411,6 +412,12 @@ struct CalendarView: View {
|
|||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
}
|
}
|
||||||
|
.sheet(item: $editingTask) { task in
|
||||||
|
TaskEditSheet(task: task)
|
||||||
|
.environmentObject(taskVM)
|
||||||
|
.presentationDetents([.medium, .large])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
calStore.refreshStatus()
|
calStore.refreshStatus()
|
||||||
if calStore.isAuthorized { calStore.fetchMonth(containing: displayMonth) }
|
if calStore.isAuthorized { calStore.fetchMonth(containing: displayMonth) }
|
||||||
@@ -602,8 +609,10 @@ struct CalendarView: View {
|
|||||||
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
|
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
|
||||||
},
|
},
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -1583,6 +1592,7 @@ struct DayTimelineView: View {
|
|||||||
var onEdit: ((TaskItem) -> Void)? = nil
|
var onEdit: ((TaskItem) -> Void)? = nil
|
||||||
var onDelete: ((TaskItem) -> Void)? = nil
|
var onDelete: ((TaskItem) -> Void)? = nil
|
||||||
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
|
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
|
||||||
|
var onPostponeMinutes: ((TaskItem, Int) -> Void)? = nil
|
||||||
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
||||||
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
||||||
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
||||||
@@ -1746,6 +1756,7 @@ struct DayTimelineView: View {
|
|||||||
task: task,
|
task: task,
|
||||||
onPin: { onPin?($0) },
|
onPin: { onPin?($0) },
|
||||||
onSetDate: { onSetDate?($0, $1) },
|
onSetDate: { onSetDate?($0, $1) },
|
||||||
|
onPostponeMinutes: { onPostponeMinutes?($0, $1) },
|
||||||
onMove: { onMove?($0, $1) },
|
onMove: { onMove?($0, $1) },
|
||||||
onSetPriority: { onSetPriority?($0, $1) },
|
onSetPriority: { onSetPriority?($0, $1) },
|
||||||
onSetCategory: { onSetCategory?($0, $1) },
|
onSetCategory: { onSetCategory?($0, $1) },
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ struct MatrixView: View {
|
|||||||
@State private var showUserGuide = false
|
@State private var showUserGuide = false
|
||||||
@State private var drillQuadrant: Quadrant? = nil
|
@State private var drillQuadrant: Quadrant? = nil
|
||||||
@State private var showDrill = false
|
@State private var showDrill = false
|
||||||
|
@State private var editingTask: TaskItem? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
@@ -64,10 +65,12 @@ struct MatrixView: View {
|
|||||||
onDropExit: { dropTarget = nil },
|
onDropExit: { dropTarget = nil },
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
|
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -83,10 +86,12 @@ struct MatrixView: View {
|
|||||||
onDropExit: { dropTarget = nil },
|
onDropExit: { dropTarget = nil },
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
|
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -106,10 +111,12 @@ struct MatrixView: View {
|
|||||||
onDropExit: { dropTarget = nil },
|
onDropExit: { dropTarget = nil },
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
|
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -125,10 +132,12 @@ struct MatrixView: View {
|
|||||||
onDropExit: { dropTarget = nil },
|
onDropExit: { dropTarget = nil },
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
|
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
|
||||||
)
|
)
|
||||||
@@ -155,6 +164,12 @@ struct MatrixView: View {
|
|||||||
.presentationDetents([.medium])
|
.presentationDetents([.medium])
|
||||||
.presentationDragIndicator(.visible)
|
.presentationDragIndicator(.visible)
|
||||||
}
|
}
|
||||||
|
.sheet(item: $editingTask) { task in
|
||||||
|
TaskEditSheet(task: task)
|
||||||
|
.environmentObject(taskVM)
|
||||||
|
.presentationDetents([.medium, .large])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
}
|
||||||
.navigationDestination(isPresented: $showDrill) {
|
.navigationDestination(isPresented: $showDrill) {
|
||||||
if let q = drillQuadrant {
|
if let q = drillQuadrant {
|
||||||
QuadrantDetailView(quadrant: q)
|
QuadrantDetailView(quadrant: q)
|
||||||
@@ -189,10 +204,12 @@ struct QuadrantCard: View {
|
|||||||
let onDropExit: () -> Void
|
let onDropExit: () -> Void
|
||||||
var onPin: ((TaskItem) -> Void)? = nil
|
var onPin: ((TaskItem) -> Void)? = nil
|
||||||
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
|
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
|
||||||
|
var onPostponeMinutes: ((TaskItem, Int) -> Void)? = nil
|
||||||
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
||||||
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
||||||
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
||||||
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
|
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
|
||||||
|
var onEdit: ((TaskItem) -> Void)? = nil
|
||||||
var onDelete: ((TaskItem) -> Void)? = nil
|
var onDelete: ((TaskItem) -> Void)? = nil
|
||||||
var onTapHeader: (() -> Void)? = nil
|
var onTapHeader: (() -> Void)? = nil
|
||||||
|
|
||||||
@@ -302,11 +319,13 @@ struct QuadrantCard: View {
|
|||||||
currentQuadrant: quadrant,
|
currentQuadrant: quadrant,
|
||||||
onPin: { t in withAnimation(KisaniSpring.snappy) { onPin?(t) } },
|
onPin: { t in withAnimation(KisaniSpring.snappy) { onPin?(t) } },
|
||||||
onSetDate: { onSetDate?($0, $1) },
|
onSetDate: { onSetDate?($0, $1) },
|
||||||
|
onPostponeMinutes: { onPostponeMinutes?($0, $1) },
|
||||||
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
|
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
|
||||||
onSetPriority: { onSetPriority?($0, $1) },
|
onSetPriority: { onSetPriority?($0, $1) },
|
||||||
onSetCategory: { onSetCategory?($0, $1) },
|
onSetCategory: { onSetCategory?($0, $1) },
|
||||||
onResetMatrixUrgency: { t in withAnimation(KisaniSpring.snappy) { onResetMatrixUrgency?(t) } },
|
onResetMatrixUrgency: { t in withAnimation(KisaniSpring.snappy) { onResetMatrixUrgency?(t) } },
|
||||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||||
|
onEdit: onEdit.map { cb in { cb($0) } },
|
||||||
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
|
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -461,6 +480,7 @@ struct QuadrantDetailView: View {
|
|||||||
currentQuadrant: quadrant,
|
currentQuadrant: quadrant,
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -487,6 +507,7 @@ struct QuadrantDetailView: View {
|
|||||||
currentQuadrant: quadrant,
|
currentQuadrant: quadrant,
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -514,6 +535,7 @@ struct QuadrantDetailView: View {
|
|||||||
currentQuadrant: quadrant,
|
currentQuadrant: quadrant,
|
||||||
onPin: { taskVM.pin($0) },
|
onPin: { taskVM.pin($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ struct TaskMenuItems: View {
|
|||||||
|
|
||||||
var onPin: (TaskItem) -> Void = { _ in }
|
var onPin: (TaskItem) -> Void = { _ in }
|
||||||
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
||||||
|
var onPostponeMinutes: (TaskItem, Int) -> Void = { _, _ in }
|
||||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||||
@@ -69,8 +70,29 @@ struct TaskMenuItems: View {
|
|||||||
} label: { Label("Date", systemImage: "calendar.badge.clock") }
|
} label: { Label("Date", systemImage: "calendar.badge.clock") }
|
||||||
|
|
||||||
Menu {
|
Menu {
|
||||||
ForEach(Quadrant.allCases.filter { $0 != (currentQuadrant ?? task.quadrant) }) { q in
|
Button { onPostponeMinutes(task, 15) } label: {
|
||||||
Button { onMove(task, q) } label: { Text(q.rawValue) }
|
Label("15 Minutes", systemImage: "clock")
|
||||||
|
}
|
||||||
|
Button { onPostponeMinutes(task, 30) } label: {
|
||||||
|
Label("30 Minutes", systemImage: "clock")
|
||||||
|
}
|
||||||
|
Button { onPostponeMinutes(task, 60) } label: {
|
||||||
|
Label("1 Hour", systemImage: "clock")
|
||||||
|
}
|
||||||
|
Button { onPostponeMinutes(task, 120) } label: {
|
||||||
|
Label("2 Hours", systemImage: "clock")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
Button { onSetDate(task, preservingTime(on: dayOffset(1))) } label: {
|
||||||
|
Label("Tomorrow", systemImage: "sun.max")
|
||||||
|
}
|
||||||
|
} label: { Label("Postpone", systemImage: "clock.arrow.circlepath") }
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
ForEach(Quadrant.allCases) { q in
|
||||||
|
Button { onMove(task, q) } label: {
|
||||||
|
Label(q.rawValue, systemImage: q == (currentQuadrant ?? task.quadrant) ? "checkmark" : "square")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} label: { Label("Move", systemImage: "arrow.right.square") }
|
} label: { Label("Move", systemImage: "arrow.right.square") }
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ struct TodayView: View {
|
|||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -129,6 +130,7 @@ struct TodayView: View {
|
|||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -149,6 +151,7 @@ struct TodayView: View {
|
|||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -167,6 +170,7 @@ struct TodayView: View {
|
|||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -185,6 +189,7 @@ struct TodayView: View {
|
|||||||
onEdit: { editingTask = $0 },
|
onEdit: { editingTask = $0 },
|
||||||
onDelete: { taskVM.delete($0) },
|
onDelete: { taskVM.delete($0) },
|
||||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||||
|
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
|
||||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||||
@@ -540,6 +545,7 @@ struct OverdueCard: View {
|
|||||||
var onEdit: (TaskItem) -> Void = { _ in }
|
var onEdit: (TaskItem) -> Void = { _ in }
|
||||||
var onDelete: (TaskItem) -> Void = { _ in }
|
var onDelete: (TaskItem) -> Void = { _ in }
|
||||||
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
||||||
|
var onPostponeMinutes: (TaskItem, Int) -> Void = { _, _ in }
|
||||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||||
@@ -642,6 +648,7 @@ struct OverdueCard: View {
|
|||||||
task: task,
|
task: task,
|
||||||
onPin: onPin,
|
onPin: onPin,
|
||||||
onSetDate: onSetDate,
|
onSetDate: onSetDate,
|
||||||
|
onPostponeMinutes: onPostponeMinutes,
|
||||||
onMove: onMove,
|
onMove: onMove,
|
||||||
onSetPriority: onSetPriority,
|
onSetPriority: onSetPriority,
|
||||||
onSetCategory: onSetCategory,
|
onSetCategory: onSetCategory,
|
||||||
@@ -955,6 +962,7 @@ struct UpcomingSection: View {
|
|||||||
var onEdit: (TaskItem) -> Void = { _ in }
|
var onEdit: (TaskItem) -> Void = { _ in }
|
||||||
let onDelete: (TaskItem) -> Void
|
let onDelete: (TaskItem) -> Void
|
||||||
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
var onSetDate: (TaskItem, Date?) -> Void = { _, _ in }
|
||||||
|
var onPostponeMinutes: (TaskItem, Int) -> Void = { _, _ in }
|
||||||
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
var onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||||
@@ -1019,6 +1027,7 @@ struct UpcomingSection: View {
|
|||||||
task: task,
|
task: task,
|
||||||
onPin: onPin,
|
onPin: onPin,
|
||||||
onSetDate: onSetDate,
|
onSetDate: onSetDate,
|
||||||
|
onPostponeMinutes: onPostponeMinutes,
|
||||||
onMove: onMove,
|
onMove: onMove,
|
||||||
onSetPriority: onSetPriority,
|
onSetPriority: onSetPriority,
|
||||||
onSetCategory: onSetCategory,
|
onSetCategory: onSetCategory,
|
||||||
@@ -1930,16 +1939,13 @@ struct TaskDatePickerSheet: View {
|
|||||||
@State private var localTime: Date?
|
@State private var localTime: Date?
|
||||||
@State private var showTimePicker = false
|
@State private var showTimePicker = false
|
||||||
@State private var showReminderPicker = false
|
@State private var showReminderPicker = false
|
||||||
// Reminder lead-time, expressed as days before the task date (nil = no reminder).
|
// Reminder lead-time, expressed as MINUTES before the task time (nil = no reminder).
|
||||||
@State private var localReminderOffset: Int?
|
@State private var localReminderOffset: Int?
|
||||||
@State private var localReminderTime: Date
|
|
||||||
@State private var localConstant: Bool
|
@State private var localConstant: Bool
|
||||||
// Custom wheel state (draft — committed to the reminder only on "Add")
|
// Custom wheel state (draft — committed to the reminder only on "Add")
|
||||||
@State private var showCustomReminder = false
|
@State private var showCustomReminder = false
|
||||||
@State private var customNumber: Int
|
@State private var customNumber: Int
|
||||||
@State private var customUnitWeeks: Bool
|
@State private var customUnit: Int // 0 = minutes, 1 = hours, 2 = days
|
||||||
@State private var customHour: Int
|
|
||||||
@State private var customMinute: Int
|
|
||||||
@State private var localRepeat: String
|
@State private var localRepeat: String
|
||||||
@State private var localRecurrenceEnd: Date?
|
@State private var localRecurrenceEnd: Date?
|
||||||
@State private var showRecurrenceEndPicker = false
|
@State private var showRecurrenceEndPicker = false
|
||||||
@@ -1972,30 +1978,24 @@ struct TaskDatePickerSheet: View {
|
|||||||
_pickerMode = State(initialValue: endDate.wrappedValue != nil ? 1 : 0)
|
_pickerMode = State(initialValue: endDate.wrappedValue != nil ? 1 : 0)
|
||||||
_localConstant = State(initialValue: constantReminder.wrappedValue)
|
_localConstant = State(initialValue: constantReminder.wrappedValue)
|
||||||
|
|
||||||
// Derive lead-time + time-of-day from any existing absolute reminder date.
|
// Derive the lead-time (minutes before the event time) from any existing reminder.
|
||||||
if let r = reminderDate.wrappedValue {
|
if let r = reminderDate.wrappedValue {
|
||||||
let days = cal.dateComponents([.day],
|
let base: Date = hasTime.wrappedValue
|
||||||
from: cal.startOfDay(for: r),
|
? (selectedDate.wrappedValue ?? start)
|
||||||
to: cal.startOfDay(for: start)).day ?? 0
|
: (cal.date(bySettingHour: 9, minute: 0, second: 0, of: start) ?? start)
|
||||||
let offset = max(0, days)
|
let mins = max(0, cal.dateComponents([.minute], from: r, to: base).minute ?? 0)
|
||||||
_localReminderOffset = State(initialValue: offset)
|
_localReminderOffset = State(initialValue: mins)
|
||||||
_localReminderTime = State(initialValue: r)
|
if mins > 0 && mins % 1440 == 0 {
|
||||||
if offset > 0 && offset % 7 == 0 {
|
_customUnit = State(initialValue: 2); _customNumber = State(initialValue: mins / 1440)
|
||||||
_customUnitWeeks = State(initialValue: true)
|
} else if mins > 0 && mins % 60 == 0 {
|
||||||
_customNumber = State(initialValue: offset / 7)
|
_customUnit = State(initialValue: 1); _customNumber = State(initialValue: mins / 60)
|
||||||
} else {
|
} else {
|
||||||
_customUnitWeeks = State(initialValue: false)
|
_customUnit = State(initialValue: 0); _customNumber = State(initialValue: max(1, mins))
|
||||||
_customNumber = State(initialValue: offset)
|
|
||||||
}
|
}
|
||||||
_customHour = State(initialValue: cal.component(.hour, from: r))
|
|
||||||
_customMinute = State(initialValue: cal.component(.minute, from: r))
|
|
||||||
} else {
|
} else {
|
||||||
_localReminderOffset = State(initialValue: nil)
|
_localReminderOffset = State(initialValue: nil)
|
||||||
_localReminderTime = State(initialValue: cal.date(bySettingHour: 9, minute: 0, second: 0, of: start) ?? start)
|
_customUnit = State(initialValue: 1) // default Custom draft: 1 hour
|
||||||
_customUnitWeeks = State(initialValue: false)
|
_customNumber = State(initialValue: 1)
|
||||||
_customNumber = State(initialValue: 0)
|
|
||||||
_customHour = State(initialValue: 9)
|
|
||||||
_customMinute = State(initialValue: 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2158,14 +2158,15 @@ struct TaskDatePickerSheet: View {
|
|||||||
let label: String
|
let label: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Offsets are MINUTES before the event time.
|
||||||
private var reminderPresets: [ReminderPreset] {
|
private var reminderPresets: [ReminderPreset] {
|
||||||
[
|
[
|
||||||
ReminderPreset(offset: nil, label: "None"),
|
ReminderPreset(offset: nil, label: "None"),
|
||||||
ReminderPreset(offset: 0, label: "On the day"),
|
ReminderPreset(offset: 0, label: "On time"),
|
||||||
ReminderPreset(offset: 1, label: "1 day early"),
|
ReminderPreset(offset: 5, label: "5 minutes early"),
|
||||||
ReminderPreset(offset: 2, label: "2 days early"),
|
ReminderPreset(offset: 30, label: "30 minutes early"),
|
||||||
ReminderPreset(offset: 3, label: "3 days early"),
|
ReminderPreset(offset: 60, label: "1 hour early"),
|
||||||
ReminderPreset(offset: 7, label: "1 week early"),
|
ReminderPreset(offset: 1440, label: "1 day early"),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2176,12 +2177,7 @@ struct TaskDatePickerSheet: View {
|
|||||||
Button {
|
Button {
|
||||||
withAnimation(KisaniSpring.micro) {
|
withAnimation(KisaniSpring.micro) {
|
||||||
localReminderOffset = preset.offset
|
localReminderOffset = preset.offset
|
||||||
if let off = preset.offset {
|
if preset.offset == nil { localConstant = false }
|
||||||
if off > 0 && off % 7 == 0 { customUnitWeeks = true; customNumber = off / 7 }
|
|
||||||
else { customUnitWeeks = false; customNumber = off }
|
|
||||||
} else {
|
|
||||||
localConstant = false
|
|
||||||
}
|
|
||||||
showCustomReminder = false
|
showCustomReminder = false
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
@@ -2189,11 +2185,6 @@ struct TaskDatePickerSheet: View {
|
|||||||
Text(preset.label)
|
Text(preset.label)
|
||||||
.font(AppFonts.sans(15))
|
.font(AppFonts.sans(15))
|
||||||
.foregroundColor(preset.offset == nil ? AppColors.accent : .primary)
|
.foregroundColor(preset.offset == nil ? AppColors.accent : .primary)
|
||||||
if preset.offset != nil {
|
|
||||||
Text("(\(timeFmt.string(from: localReminderTime)))")
|
|
||||||
.font(AppFonts.sans(13))
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
|
||||||
Spacer()
|
Spacer()
|
||||||
if !showCustomReminder && localReminderOffset == preset.offset {
|
if !showCustomReminder && localReminderOffset == preset.offset {
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
@@ -2242,38 +2233,34 @@ struct TaskDatePickerSheet: View {
|
|||||||
|
|
||||||
// MARK: - Custom reminder wheel (Day / Week)
|
// MARK: - Custom reminder wheel (Day / Week)
|
||||||
|
|
||||||
|
// Minutes-per-unit for the Custom wheel: minutes / hours / days.
|
||||||
|
private let customUnitMinutes = [1, 60, 1440]
|
||||||
|
private let customUnitNames = ["minute", "hour", "day"]
|
||||||
|
|
||||||
private func seedCustomDraft() {
|
private func seedCustomDraft() {
|
||||||
let cal = Calendar.current
|
guard let off = localReminderOffset, off > 0 else { return }
|
||||||
if let off = localReminderOffset {
|
if off % 1440 == 0 { customUnit = 2; customNumber = off / 1440 }
|
||||||
if off > 0 && off % 7 == 0 { customUnitWeeks = true; customNumber = off / 7 }
|
else if off % 60 == 0 { customUnit = 1; customNumber = off / 60 }
|
||||||
else { customUnitWeeks = false; customNumber = off }
|
else { customUnit = 0; customNumber = off }
|
||||||
}
|
|
||||||
customHour = cal.component(.hour, from: localReminderTime)
|
|
||||||
customMinute = cal.component(.minute, from: localReminderTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var customOffsetMinutes: Int { max(1, customNumber) * customUnitMinutes[customUnit] }
|
||||||
|
|
||||||
private func commitCustomReminder() {
|
private func commitCustomReminder() {
|
||||||
let cal = Calendar.current
|
localReminderOffset = customOffsetMinutes
|
||||||
localReminderOffset = customUnitWeeks ? customNumber * 7 : customNumber
|
|
||||||
localReminderTime = cal.date(bySettingHour: customHour, minute: customMinute,
|
|
||||||
second: 0, of: localDate) ?? localReminderTime
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label for a given count under the current Day/Week mode.
|
// Label for a given count under the current unit.
|
||||||
private func customLeadLabel(_ n: Int) -> String {
|
private func customLeadLabel(_ n: Int) -> String {
|
||||||
if n == 0 { return "On the day" }
|
let unit = customUnitNames[customUnit]
|
||||||
let unit = customUnitWeeks ? "week" : "day"
|
|
||||||
return "\(n) \(unit)\(n == 1 ? "" : "s") early"
|
return "\(n) \(unit)\(n == 1 ? "" : "s") early"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var customMaxCount: Int { customUnitWeeks ? 8 : 60 }
|
private var customMaxCount: Int { [59, 23, 30][customUnit] }
|
||||||
|
|
||||||
// Reminder date implied by the current *draft* wheel selection.
|
// Reminder date implied by the current *draft* wheel selection.
|
||||||
private var draftReminderDate: Date? {
|
private var draftReminderDate: Date? {
|
||||||
let cal = Calendar.current
|
Calendar.current.date(byAdding: .minute, value: -customOffsetMinutes, to: reminderBaseDate())
|
||||||
let off = customUnitWeeks ? customNumber * 7 : customNumber
|
|
||||||
let base = cal.date(byAdding: .day, value: -off, to: localDate) ?? localDate
|
|
||||||
return cal.date(bySettingHour: customHour, minute: customMinute, second: 0, of: base)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var draftReminderInPast: Bool {
|
private var draftReminderInPast: Bool {
|
||||||
@@ -2289,42 +2276,30 @@ struct TaskDatePickerSheet: View {
|
|||||||
.onTapGesture { withAnimation(KisaniSpring.snappy) { showCustomReminder = false } }
|
.onTapGesture { withAnimation(KisaniSpring.snappy) { showCustomReminder = false } }
|
||||||
|
|
||||||
VStack(spacing: 18) {
|
VStack(spacing: 18) {
|
||||||
// Day / Week segmented toggle
|
// Minutes / Hours / Days segmented toggle
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
customModeButton("Day", weeks: false)
|
customModeButton("Minutes", unit: 0)
|
||||||
customModeButton("Week", weeks: true)
|
customModeButton("Hours", unit: 1)
|
||||||
|
customModeButton("Days", unit: 2)
|
||||||
}
|
}
|
||||||
.padding(3)
|
.padding(3)
|
||||||
.background(Color(uiColor: .tertiarySystemFill))
|
.background(Color(uiColor: .tertiarySystemFill))
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
|
|
||||||
// Three-column wheel: lead-time · hour · minute
|
// Single lead-time wheel (relative to the event time)
|
||||||
HStack(spacing: 0) {
|
Picker("", selection: $customNumber) {
|
||||||
Picker("", selection: $customNumber) {
|
ForEach(1...customMaxCount, id: \.self) { n in
|
||||||
ForEach(0...customMaxCount, id: \.self) { n in
|
Text(customLeadLabel(n)).tag(n)
|
||||||
Text(customLeadLabel(n)).tag(n)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.pickerStyle(.wheel).frame(maxWidth: .infinity).clipped()
|
|
||||||
|
|
||||||
Picker("", selection: $customHour) {
|
|
||||||
ForEach(0...23, id: \.self) { h in
|
|
||||||
Text(String(format: "%02d", h)).tag(h)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.wheel).frame(width: 64).clipped()
|
|
||||||
|
|
||||||
Picker("", selection: $customMinute) {
|
|
||||||
ForEach(0...59, id: \.self) { m in
|
|
||||||
Text(String(format: "%02d", m)).tag(m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.pickerStyle(.wheel).frame(width: 64).clipped()
|
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.frame(height: 180)
|
.frame(height: 180)
|
||||||
.onChange(of: customUnitWeeks) { _ in
|
.clipped()
|
||||||
|
.onChange(of: customUnit) { _ in
|
||||||
if customNumber > customMaxCount { customNumber = customMaxCount }
|
if customNumber > customMaxCount { customNumber = customMaxCount }
|
||||||
|
if customNumber < 1 { customNumber = 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtle (non-red) note — never alarming.
|
// Subtle (non-red) note — never alarming.
|
||||||
@@ -2371,15 +2346,19 @@ struct TaskDatePickerSheet: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func customModeButton(_ label: String, weeks: Bool) -> some View {
|
private func customModeButton(_ label: String, unit: Int) -> some View {
|
||||||
Button {
|
Button {
|
||||||
withAnimation(KisaniSpring.micro) { customUnitWeeks = weeks }
|
withAnimation(KisaniSpring.micro) {
|
||||||
|
customUnit = unit
|
||||||
|
if customNumber > customMaxCount { customNumber = customMaxCount }
|
||||||
|
if customNumber < 1 { customNumber = 1 }
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(AppFonts.sans(15, weight: customUnitWeeks == weeks ? .semibold : .regular))
|
.font(AppFonts.sans(15, weight: customUnit == unit ? .semibold : .regular))
|
||||||
.foregroundColor(customUnitWeeks == weeks ? .primary : .secondary)
|
.foregroundColor(customUnit == unit ? .primary : .secondary)
|
||||||
.frame(width: 96, height: 36)
|
.frame(maxWidth: .infinity).frame(height: 36)
|
||||||
.background(customUnitWeeks == weeks ? Color(uiColor: .systemGray4) : Color.clear)
|
.background(customUnit == unit ? Color(uiColor: .systemGray4) : Color.clear)
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@@ -2390,22 +2369,26 @@ struct TaskDatePickerSheet: View {
|
|||||||
return computedReminderDate(offset: off) ?? Date() < Date()
|
return computedReminderDate(offset: off) ?? Date() < Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func computedReminderDate(offset: Int) -> Date? {
|
// The event datetime the reminder counts back from: the task's time, or 9 AM if undated.
|
||||||
|
private func reminderBaseDate() -> Date {
|
||||||
let cal = Calendar.current
|
let cal = Calendar.current
|
||||||
let base = cal.date(byAdding: .day, value: -offset, to: localDate) ?? localDate
|
if let t = localTime {
|
||||||
let tc = cal.dateComponents([.hour, .minute], from: localReminderTime)
|
let tc = cal.dateComponents([.hour, .minute], from: t)
|
||||||
return cal.date(bySettingHour: tc.hour ?? 9, minute: tc.minute ?? 0, second: 0, of: base)
|
return cal.date(bySettingHour: tc.hour ?? 9, minute: tc.minute ?? 0, second: 0, of: localDate) ?? localDate
|
||||||
|
}
|
||||||
|
return cal.date(bySettingHour: 9, minute: 0, second: 0, of: localDate) ?? localDate
|
||||||
|
}
|
||||||
|
|
||||||
|
private func computedReminderDate(offset: Int) -> Date? {
|
||||||
|
Calendar.current.date(byAdding: .minute, value: -offset, to: reminderBaseDate())
|
||||||
}
|
}
|
||||||
|
|
||||||
private var reminderSummary: String {
|
private var reminderSummary: String {
|
||||||
guard let off = localReminderOffset else { return "None" }
|
guard let off = localReminderOffset else { return "None" }
|
||||||
let timeStr = timeFmt.string(from: localReminderTime)
|
if off == 0 { return "On time" }
|
||||||
if off == 0 { return "On the day · \(timeStr)" }
|
if off % 1440 == 0 { let d = off / 1440; return "\(d) day\(d == 1 ? "" : "s") early" }
|
||||||
if off % 7 == 0 {
|
if off % 60 == 0 { let h = off / 60; return "\(h) hour\(h == 1 ? "" : "s") early" }
|
||||||
let w = off / 7
|
return "\(off) minute\(off == 1 ? "" : "s") early"
|
||||||
return "\(w) week\(w == 1 ? "" : "s") early · \(timeStr)"
|
|
||||||
}
|
|
||||||
return "\(off) day\(off == 1 ? "" : "s") early · \(timeStr)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
Reference in New Issue
Block a user