Add unified task menu, Live Activity, and calendar event actions
Task context menu (new shared TaskMenuItems) consistent across Today, Matrix, and Calendar: Pin · Date · Move · Priority · Tags · Add to Live Activity · Delete. Adds TaskViewModel.setPriority. Live Activity (ActivityKit): - TaskActivityAttributes shared between app and widget targets - LiveActivityManager (start/update/end/toggle, iOS 16.1+) - TaskLiveActivity widget: lock-screen banner + Dynamic Island - NSSupportsLiveActivities via project.yml; project regenerated Calendar events (non-subscription, writable only): - Edit via system EKEventEditViewController (EventEditView wrapper) - Delete via new CalendarStore.deleteEvent Also sweeps in prior in-progress edits already present in the working tree (AuthManager, NotificationManager, TaskItem, task views) and the updated ISSUES.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import SwiftUI
|
||||
import EventKit
|
||||
import EventKitUI
|
||||
|
||||
// MARK: - Calendar Store
|
||||
@MainActor
|
||||
@@ -115,6 +116,20 @@ final class CalendarStore: ObservableObject {
|
||||
return authStatus == .authorized
|
||||
}
|
||||
|
||||
/// Exposed so the edit sheet can hand it to EKEventEditViewController.
|
||||
var eventStore: EKEventStore { ekStore }
|
||||
|
||||
/// Delete a real (writable) calendar event, then refresh the month cache.
|
||||
func deleteEvent(_ event: EKEvent) {
|
||||
guard event.calendar?.allowsContentModifications == true else { return }
|
||||
do {
|
||||
try ekStore.remove(event, span: .thisEvent)
|
||||
fetchMonth(containing: event.startDate ?? Date())
|
||||
} catch {
|
||||
print("Delete event failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Only `.notDetermined` can show the system prompt; otherwise we must deep-link to Settings.
|
||||
var canRequest: Bool { authStatus == .notDetermined }
|
||||
var isDenied: Bool { authStatus == .denied || authStatus == .restricted }
|
||||
@@ -227,6 +242,7 @@ struct CalendarView: View {
|
||||
@State private var showAddTask = false
|
||||
@State private var showCalendarConnect = false
|
||||
@State private var showWorkoutSession = false
|
||||
@State private var editingEvent: EditableEvent? = nil
|
||||
|
||||
private let cal = Calendar.current
|
||||
private let weekdays = ["S","M","T","W","T","F","S"]
|
||||
@@ -334,6 +350,12 @@ struct CalendarView: View {
|
||||
.presentationDetents([.large])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
.sheet(item: $editingEvent) { wrapper in
|
||||
EventEditView(event: wrapper.event, store: calStore.eventStore) {
|
||||
calStore.fetchMonth(containing: selectedDate)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.onAppear {
|
||||
calStore.refreshStatus()
|
||||
if calStore.isAuthorized { calStore.fetchMonth(containing: displayMonth) }
|
||||
@@ -517,7 +539,15 @@ struct CalendarView: View {
|
||||
},
|
||||
onToggle: { task in
|
||||
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
|
||||
}
|
||||
},
|
||||
onPin: { taskVM.pin($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) },
|
||||
onEditEvent: { editingEvent = EditableEvent(event: $0) },
|
||||
onDeleteEvent: { calStore.deleteEvent($0) }
|
||||
)
|
||||
.padding(.horizontal, 16).padding(.top, 12).padding(.bottom, 20)
|
||||
}
|
||||
@@ -1442,6 +1472,7 @@ private struct DayTLEntry: Identifiable {
|
||||
var sortGroup: Int = 0 // 0 = active, 1 = completed, 2 = overdue
|
||||
var taskRef: TaskItem?
|
||||
var workoutRef: WorkoutProgram?
|
||||
var eventRef: EKEvent?
|
||||
}
|
||||
|
||||
struct DayTimelineView: View {
|
||||
@@ -1458,6 +1489,13 @@ struct DayTimelineView: View {
|
||||
var onPostpone: ((TaskItem) -> Void)? = nil
|
||||
var onEdit: ((TaskItem) -> Void)? = nil
|
||||
var onDelete: ((TaskItem) -> Void)? = nil
|
||||
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
|
||||
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
||||
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
||||
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
||||
// Calendar events (non-subscription only): edit/delete in the real calendar.
|
||||
var onEditEvent: ((EKEvent) -> Void)? = nil
|
||||
var onDeleteEvent: ((EKEvent) -> Void)? = nil
|
||||
|
||||
private let cal = Calendar.current
|
||||
private static let overdueFmt: DateFormatter = {
|
||||
@@ -1485,7 +1523,8 @@ struct DayTimelineView: View {
|
||||
timeLabel: "All Day",
|
||||
title: ev.title ?? "Event",
|
||||
color: Color(cgColor: ev.calendar.cgColor),
|
||||
isComplete: false, isAllDay: true, sortKey: -1, sortGroup: 0
|
||||
isComplete: false, isAllDay: true, sortKey: -1, sortGroup: 0,
|
||||
eventRef: ev
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1533,7 +1572,8 @@ struct DayTimelineView: View {
|
||||
timeLabel: "\(hd)\(ms)", ampm: h < 12 ? "AM" : "PM",
|
||||
title: ev.title ?? "Event", subtitle: endLabel,
|
||||
color: Color(cgColor: ev.calendar.cgColor),
|
||||
isComplete: false, isAllDay: false, sortKey: h * 60 + m, sortGroup: 0
|
||||
isComplete: false, isAllDay: false, sortKey: h * 60 + m, sortGroup: 0,
|
||||
eventRef: ev
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1587,6 +1627,11 @@ struct DayTimelineView: View {
|
||||
)
|
||||
if let t = entry.taskRef {
|
||||
row.contextMenu { taskContextMenu(t) }
|
||||
} else if let ev = entry.eventRef,
|
||||
onEditEvent != nil || onDeleteEvent != nil,
|
||||
ev.calendar?.type != .subscription,
|
||||
ev.calendar?.allowsContentModifications == true {
|
||||
row.contextMenu { eventContextMenu(ev) }
|
||||
} else {
|
||||
row
|
||||
}
|
||||
@@ -1597,26 +1642,30 @@ struct DayTimelineView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func taskContextMenu(_ task: TaskItem) -> some View {
|
||||
Button { onToggle?(task) } label: {
|
||||
Label(task.isComplete ? "Mark Incomplete" : "Mark Complete", systemImage: "checkmark.circle")
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
onPin: { onPin?($0) },
|
||||
onSetDate: { onSetDate?($0, $1) },
|
||||
onMove: { onMove?($0, $1) },
|
||||
onSetPriority: { onSetPriority?($0, $1) },
|
||||
onSetCategory: { onSetCategory?($0, $1) },
|
||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||
onEdit: onEdit,
|
||||
onDelete: { onDelete?($0) }
|
||||
)
|
||||
}
|
||||
|
||||
// Edit / Delete for a real (non-subscription) calendar event.
|
||||
@ViewBuilder
|
||||
private func eventContextMenu(_ event: EKEvent) -> some View {
|
||||
if let onEditEvent {
|
||||
Button { onEditEvent(event) } label: { Label("Edit", systemImage: "pencil") }
|
||||
}
|
||||
if let onPostpone {
|
||||
Button { onPostpone(task) } label: {
|
||||
Label("Postpone 1 Day", systemImage: "clock.arrow.circlepath")
|
||||
}
|
||||
}
|
||||
if let onPin {
|
||||
Button { onPin(task) } label: {
|
||||
Label(task.isPinned ? "Unpin" : "Pin", systemImage: task.isPinned ? "pin.slash" : "pin")
|
||||
}
|
||||
}
|
||||
if let onEdit {
|
||||
Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") }
|
||||
}
|
||||
ShareLink(item: task.title) { Label("Share", systemImage: "square.and.arrow.up") }
|
||||
if let onDelete {
|
||||
if let onDeleteEvent {
|
||||
Divider()
|
||||
Button(role: .destructive) { onDelete(task) } label: { Label("Delete", systemImage: "trash") }
|
||||
Button(role: .destructive) { onDeleteEvent(event) } label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1816,3 +1865,40 @@ struct WorkoutSectionBar: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Event Edit Sheet
|
||||
|
||||
/// Identifiable wrapper so an EKEvent can drive `.sheet(item:)`.
|
||||
struct EditableEvent: Identifiable {
|
||||
let id = UUID()
|
||||
let event: EKEvent
|
||||
}
|
||||
|
||||
/// Wraps the system event editor (EKEventEditViewController).
|
||||
struct EventEditView: UIViewControllerRepresentable {
|
||||
let event: EKEvent
|
||||
let store: EKEventStore
|
||||
var onDone: () -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> EKEventEditViewController {
|
||||
let vc = EKEventEditViewController()
|
||||
vc.eventStore = store
|
||||
vc.event = event
|
||||
vc.editViewDelegate = context.coordinator
|
||||
return vc
|
||||
}
|
||||
|
||||
func updateUIViewController(_ vc: EKEventEditViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator { Coordinator(onDone: onDone) }
|
||||
|
||||
final class Coordinator: NSObject, EKEventEditViewDelegate {
|
||||
let onDone: () -> Void
|
||||
init(onDone: @escaping () -> Void) { self.onDone = onDone }
|
||||
func eventEditViewController(_ controller: EKEventEditViewController,
|
||||
didCompleteWith action: EKEventEditViewAction) {
|
||||
controller.dismiss(animated: true)
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ struct MatrixView: View {
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
|
||||
)
|
||||
@@ -83,6 +84,7 @@ struct MatrixView: View {
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
|
||||
)
|
||||
@@ -104,6 +106,7 @@ struct MatrixView: View {
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
|
||||
)
|
||||
@@ -121,6 +124,7 @@ struct MatrixView: View {
|
||||
onSetDate: { taskVM.setDate($0, date: $1) },
|
||||
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
|
||||
onSetCategory: { taskVM.setCategory($0, category: $1) },
|
||||
onSetPriority: { taskVM.setPriority($0, priority: $1) },
|
||||
onDelete: { taskVM.delete($0) },
|
||||
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
|
||||
)
|
||||
@@ -183,6 +187,7 @@ struct QuadrantCard: View {
|
||||
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
|
||||
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
|
||||
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
|
||||
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
|
||||
var onDelete: ((TaskItem) -> Void)? = nil
|
||||
var onTapHeader: (() -> Void)? = nil
|
||||
|
||||
@@ -278,68 +283,17 @@ struct QuadrantCard: View {
|
||||
.padding(.leading, 2)
|
||||
.draggable(task.id.uuidString)
|
||||
.contextMenu {
|
||||
Button {
|
||||
withAnimation(KisaniSpring.snappy) { onPin?(task) }
|
||||
} label: {
|
||||
Label(task.isPinned ? "Unpin" : "Pin",
|
||||
systemImage: task.isPinned ? "pin.slash" : "pin")
|
||||
}
|
||||
|
||||
Menu {
|
||||
Button {
|
||||
onSetDate?(task, Calendar.current.startOfDay(for: Date()))
|
||||
} label: { Label("Today", systemImage: "sun.max") }
|
||||
Button {
|
||||
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
|
||||
onSetDate?(task, Calendar.current.startOfDay(for: tomorrow))
|
||||
} label: { Label("Tomorrow", systemImage: "sunrise") }
|
||||
Button {
|
||||
let nextWeek = Calendar.current.date(byAdding: .weekOfYear, value: 1, to: Date())!
|
||||
onSetDate?(task, Calendar.current.startOfDay(for: nextWeek))
|
||||
} label: { Label("Next Week", systemImage: "calendar") }
|
||||
Divider()
|
||||
Button {
|
||||
onSetDate?(task, nil)
|
||||
} label: { Label("Remove Date", systemImage: "xmark.circle") }
|
||||
} label: {
|
||||
Label("Date", systemImage: "calendar.badge.clock")
|
||||
}
|
||||
|
||||
Menu {
|
||||
ForEach(Quadrant.allCases.filter { $0 != quadrant }) { q in
|
||||
Button {
|
||||
withAnimation(KisaniSpring.snappy) { onMove?(task, q) }
|
||||
} label: {
|
||||
Text(q.rawValue)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Label("Move", systemImage: "arrow.right.square")
|
||||
}
|
||||
|
||||
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 { } label: {
|
||||
Label("Add to Live Activity", systemImage: "pin.circle")
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(role: .destructive) {
|
||||
withAnimation(KisaniSpring.snappy) { onDelete?(task) }
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
currentQuadrant: quadrant,
|
||||
onPin: { t in withAnimation(KisaniSpring.snappy) { onPin?(t) } },
|
||||
onSetDate: { onSetDate?($0, $1) },
|
||||
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
|
||||
onSetPriority: { onSetPriority?($0, $1) },
|
||||
onSetCategory: { onSetCategory?($0, $1) },
|
||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,6 +438,20 @@ struct QuadrantDetailView: View {
|
||||
QDTaskRow(task: task, quadrant: quadrant,
|
||||
onTap: { editingTask = task },
|
||||
onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } })
|
||||
.contextMenu {
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
currentQuadrant: quadrant,
|
||||
onPin: { taskVM.pin($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) },
|
||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
)
|
||||
}
|
||||
if task.id != overdueTasks.last?.id { Divider().padding(.leading, 46) }
|
||||
}
|
||||
}
|
||||
@@ -495,6 +463,20 @@ struct QuadrantDetailView: View {
|
||||
QDTaskRow(task: task, quadrant: quadrant,
|
||||
onTap: { editingTask = task },
|
||||
onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } })
|
||||
.contextMenu {
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
currentQuadrant: quadrant,
|
||||
onPin: { taskVM.pin($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) },
|
||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
)
|
||||
}
|
||||
if task.id != laterTasks.last?.id { Divider().padding(.leading, 46) }
|
||||
}
|
||||
}
|
||||
@@ -507,6 +489,20 @@ struct QuadrantDetailView: View {
|
||||
QDTaskRow(task: task, quadrant: quadrant,
|
||||
onTap: { editingTask = task },
|
||||
onToggle: { withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } })
|
||||
.contextMenu {
|
||||
TaskMenuItems(
|
||||
task: task,
|
||||
currentQuadrant: quadrant,
|
||||
onPin: { taskVM.pin($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) },
|
||||
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
|
||||
onEdit: { editingTask = $0 },
|
||||
onDelete: { taskVM.delete($0) }
|
||||
)
|
||||
}
|
||||
if task.id != displayed.last?.id { Divider().padding(.leading, 46) }
|
||||
}
|
||||
if completedTasks.count > completedLimit && !showAllCompleted {
|
||||
@@ -682,18 +678,20 @@ struct TaskEditSheet: View {
|
||||
@State private var endDate: Date?
|
||||
@State private var isAllDay: Bool
|
||||
@State private var reminderDate: Date?
|
||||
@State private var constantReminder: Bool
|
||||
@State private var showDatePicker = false
|
||||
|
||||
init(task: TaskItem) {
|
||||
self.task = task
|
||||
_title = State(initialValue: task.title)
|
||||
_dueDate = State(initialValue: task.dueDate)
|
||||
_hasTime = State(initialValue: task.hasTime)
|
||||
_isRecurring = State(initialValue: task.isRecurring)
|
||||
_recurrenceLabel = State(initialValue: task.recurrenceLabel)
|
||||
_endDate = State(initialValue: task.endDate)
|
||||
_isAllDay = State(initialValue: task.isAllDay)
|
||||
_reminderDate = State(initialValue: task.reminderDate)
|
||||
_title = State(initialValue: task.title)
|
||||
_dueDate = State(initialValue: task.dueDate)
|
||||
_hasTime = State(initialValue: task.hasTime)
|
||||
_isRecurring = State(initialValue: task.isRecurring)
|
||||
_recurrenceLabel = State(initialValue: task.recurrenceLabel)
|
||||
_endDate = State(initialValue: task.endDate)
|
||||
_isAllDay = State(initialValue: task.isAllDay)
|
||||
_reminderDate = State(initialValue: task.reminderDate)
|
||||
_constantReminder = State(initialValue: task.constantReminder)
|
||||
}
|
||||
|
||||
private var dateSummary: String {
|
||||
@@ -839,7 +837,8 @@ struct TaskEditSheet: View {
|
||||
recurrenceLabel: $recurrenceLabel,
|
||||
endDate: $endDate,
|
||||
isAllDay: $isAllDay,
|
||||
reminderDate: $reminderDate
|
||||
reminderDate: $reminderDate,
|
||||
constantReminder: $constantReminder
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -849,7 +848,8 @@ struct TaskEditSheet: View {
|
||||
guard !t.isEmpty else { return }
|
||||
taskVM.updateTask(task, title: t, dueDate: dueDate, hasTime: hasTime,
|
||||
isRecurring: isRecurring, recurrenceLabel: recurrenceLabel,
|
||||
endDate: endDate, isAllDay: isAllDay, reminderDate: reminderDate)
|
||||
endDate: endDate, isAllDay: isAllDay, reminderDate: reminderDate,
|
||||
constantReminder: constantReminder)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
80
KisaniCal/Views/TaskContextMenu.swift
Normal file
80
KisaniCal/Views/TaskContextMenu.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 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 onMove: (TaskItem, Quadrant) -> Void = { _, _ in }
|
||||
var onSetPriority: (TaskItem, Priority) -> Void = { _, _ in }
|
||||
var onSetCategory: (TaskItem, TaskCategory) -> Void = { _, _ in }
|
||||
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, cal.startOfDay(for: Date())) } label: {
|
||||
Label("Today", systemImage: "sun.max")
|
||||
}
|
||||
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()
|
||||
Button { onSetDate(task, nil) } label: {
|
||||
Label("Remove Date", systemImage: "xmark.circle")
|
||||
}
|
||||
} label: { Label("Date", systemImage: "calendar.badge.clock") }
|
||||
|
||||
Menu {
|
||||
ForEach(Quadrant.allCases.filter { $0 != (currentQuadrant ?? task.quadrant) }) { q in
|
||||
Button { onMove(task, q) } label: { Text(q.rawValue) }
|
||||
}
|
||||
} label: { Label("Move", systemImage: "arrow.right.square") }
|
||||
|
||||
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("Add to Live Activity", systemImage: "pin.circle")
|
||||
}
|
||||
|
||||
if let onEdit {
|
||||
Button { onEdit(task) } label: { Label("Edit", systemImage: "pencil") }
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button(role: .destructive) { onDelete(task) } label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,11 @@ struct TodayView: View {
|
||||
onPin: { taskVM.pin($0) },
|
||||
onPostpone: { taskVM.postpone($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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user