import SwiftUI import WidgetKit /// Tracks one event for the Event Countdown (Bars) widget via App Group storage. /// The widget reads these keys directly — no manual widget configuration needed. enum CountdownTracker { static let idKey = "kisani.widget.trackedEvent" static let sinceKey = "kisani.widget.trackedSince" static func isTracked(_ task: TaskItem) -> Bool { UserDefaults.kisani.string(forKey: idKey) == task.id.uuidString } static func toggle(_ task: TaskItem) { if isTracked(task) { UserDefaults.kisani.removeObject(forKey: idKey) UserDefaults.kisani.removeObject(forKey: sinceKey) } else { UserDefaults.kisani.set(task.id.uuidString, forKey: idKey) UserDefaults.kisani.set(Date(), forKey: sinceKey) // Mode A anchor } WidgetCenter.shared.reloadAllTimelines() } } /// The unified long-press menu for a task, shared across Today, Matrix, and Calendar. /// Pass only the closures a given screen needs; the rest default to no-ops. struct TaskMenuItems: View { let task: TaskItem /// The quadrant the row currently lives in (so "Move" hides the current one). var currentQuadrant: Quadrant? = nil var onPin: (TaskItem) -> Void = { _ in } var onSetDate: (TaskItem, Date?) -> Void = { _, _ in } var onPostponeMinutes: (TaskItem, Int) -> Void = { _, _ in } var onMove: (TaskItem, Quadrant) -> Void = { _, _ in } var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in } var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in } var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil var onLiveActivity:(TaskItem) -> Void = { LiveActivityManager.toggle(for: $0) } var onEdit: ((TaskItem) -> Void)? = nil var onDelete: (TaskItem) -> Void = { _ in } private var cal: Calendar { .current } var body: some View { Button { onPin(task) } label: { Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin") } Menu { 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") } Divider() if let onEdit { Button { onEdit(task) } label: { Label("Pick Date", systemImage: "calendar.badge.plus") } } Button { onSetDate(task, nil) } label: { Label("Clear", systemImage: "xmark.square") } } label: { Label("Date", systemImage: "calendar.badge.clock") } Menu { Button { onPostponeMinutes(task, 15) } label: { Label("Snooze 15 Minutes", systemImage: "clock") } Button { onPostponeMinutes(task, 30) } label: { Label("Snooze 30 Minutes", systemImage: "clock") } Button { onPostponeMinutes(task, 60) } label: { Label("Snooze 1 Hour", systemImage: "clock") } Button { onPostponeMinutes(task, 120) } label: { Label("Snooze 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 { // Only the quadrants the task is NOT currently in — you can't move it to where it sits. ForEach(Quadrant.allCases.filter { $0 != (currentQuadrant ?? task.quadrant) }) { q in Button { onMove(task, q) } label: { Label(q.matrixLabel, systemImage: "arrow.right") } } } label: { Label("Move", systemImage: "arrow.right.square") } if task.urgencyOverride != nil, let onResetMatrixUrgency { Button { onResetMatrixUrgency(task) } label: { Label("Reset Matrix Urgency", systemImage: "arrow.counterclockwise") } } Menu { ForEach(Priority.allCases) { p in Button { onSetPriority(task, p) } label: { Label(p.label, systemImage: task.priority == p ? "checkmark" : "flag") } } } label: { Label("Priority", systemImage: "flag") } Menu { ForEach([TaskCategory.reminder, .birthday, .domain, .annual, .custom], id: \.rawValue) { cat in Button { onSetCategory(task, cat) } label: { Label(cat.rawValue, systemImage: task.category == cat ? "checkmark" : "tag") } } } label: { Label("Tags", systemImage: "tag") } Button { onLiveActivity(task) } label: { Label(liveActivityTitle, systemImage: liveActivityIcon) } Button { CountdownTracker.toggle(task) } label: { Label(CountdownTracker.isTracked(task) ? "Stop Tracking Countdown" : "Track Countdown", systemImage: "timer") } if let onEdit { Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") } } Divider() Button(role: .destructive) { onDelete(task) } label: { 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 } }