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:
kutesir
2026-06-15 18:14:10 +03:00
parent 4b16e8587d
commit ac147c55b0
7 changed files with 286 additions and 119 deletions

View File

@@ -298,6 +298,7 @@ struct CalendarView: View {
@State private var showCalendarConnect = false
@State private var showWorkoutSession = false
@State private var editingEvent: EditableEvent? = nil
@State private var editingTask: TaskItem? = nil
private let cal = Calendar.current
private let weekdays = ["S","M","T","W","T","F","S"]
@@ -411,6 +412,12 @@ struct CalendarView: View {
}
.ignoresSafeArea()
}
.sheet(item: $editingTask) { task in
TaskEditSheet(task: task)
.environmentObject(taskVM)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
.onAppear {
calStore.refreshStatus()
if calStore.isAuthorized { calStore.fetchMonth(containing: displayMonth) }
@@ -602,8 +609,10 @@ struct CalendarView: View {
withAnimation(KisaniSpring.snappy) { taskVM.toggle(task) }
},
onPin: { taskVM.pin($0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -1583,6 +1592,7 @@ struct DayTimelineView: View {
var onEdit: ((TaskItem) -> Void)? = nil
var onDelete: ((TaskItem) -> Void)? = nil
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
var onPostponeMinutes: ((TaskItem, Int) -> Void)? = nil
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
@@ -1746,6 +1756,7 @@ struct DayTimelineView: View {
task: task,
onPin: { onPin?($0) },
onSetDate: { onSetDate?($0, $1) },
onPostponeMinutes: { onPostponeMinutes?($0, $1) },
onMove: { onMove?($0, $1) },
onSetPriority: { onSetPriority?($0, $1) },
onSetCategory: { onSetCategory?($0, $1) },

View File

@@ -9,6 +9,7 @@ struct MatrixView: View {
@State private var showUserGuide = false
@State private var drillQuadrant: Quadrant? = nil
@State private var showDrill = false
@State private var editingTask: TaskItem? = nil
var body: some View {
ZStack(alignment: .bottomTrailing) {
@@ -64,10 +65,12 @@ struct MatrixView: View {
onDropExit: { dropTarget = nil },
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .urgent; showDrill = true }
)
@@ -83,10 +86,12 @@ struct MatrixView: View {
onDropExit: { dropTarget = nil },
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .schedule; showDrill = true }
)
@@ -106,10 +111,12 @@ struct MatrixView: View {
onDropExit: { dropTarget = nil },
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .delegate_; showDrill = true }
)
@@ -125,10 +132,12 @@ struct MatrixView: View {
onDropExit: { dropTarget = nil },
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onResetMatrixUrgency: { taskVM.resetMatrixUrgencyOverride($0) },
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onTapHeader: { drillQuadrant = .eliminate; showDrill = true }
)
@@ -155,6 +164,12 @@ struct MatrixView: View {
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
.sheet(item: $editingTask) { task in
TaskEditSheet(task: task)
.environmentObject(taskVM)
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
.navigationDestination(isPresented: $showDrill) {
if let q = drillQuadrant {
QuadrantDetailView(quadrant: q)
@@ -189,10 +204,12 @@ struct QuadrantCard: View {
let onDropExit: () -> Void
var onPin: ((TaskItem) -> Void)? = nil
var onSetDate: ((TaskItem, Date?) -> Void)? = nil
var onPostponeMinutes: ((TaskItem, Int) -> Void)? = nil
var onMove: ((TaskItem, Quadrant) -> Void)? = nil
var onSetCategory: ((TaskItem, TaskCategory) -> Void)? = nil
var onSetPriority: ((TaskItem, Priority) -> Void)? = nil
var onResetMatrixUrgency: ((TaskItem) -> Void)? = nil
var onEdit: ((TaskItem) -> Void)? = nil
var onDelete: ((TaskItem) -> Void)? = nil
var onTapHeader: (() -> Void)? = nil
@@ -302,11 +319,13 @@ struct QuadrantCard: View {
currentQuadrant: quadrant,
onPin: { t in withAnimation(KisaniSpring.snappy) { onPin?(t) } },
onSetDate: { onSetDate?($0, $1) },
onPostponeMinutes: { onPostponeMinutes?($0, $1) },
onMove: { t, q in withAnimation(KisaniSpring.snappy) { onMove?(t, q) } },
onSetPriority: { onSetPriority?($0, $1) },
onSetCategory: { onSetCategory?($0, $1) },
onResetMatrixUrgency: { t in withAnimation(KisaniSpring.snappy) { onResetMatrixUrgency?(t) } },
onLiveActivity:{ LiveActivityManager.toggle(for: $0) },
onEdit: onEdit.map { cb in { cb($0) } },
onDelete: { t in withAnimation(KisaniSpring.snappy) { onDelete?(t) } }
)
}
@@ -461,6 +480,7 @@ struct QuadrantDetailView: View {
currentQuadrant: quadrant,
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -487,6 +507,7 @@ struct QuadrantDetailView: View {
currentQuadrant: quadrant,
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -514,6 +535,7 @@ struct QuadrantDetailView: View {
currentQuadrant: quadrant,
onPin: { taskVM.pin($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },

View File

@@ -32,6 +32,7 @@ struct TaskMenuItems: View {
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 }
@@ -69,8 +70,29 @@ struct TaskMenuItems: View {
} 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) }
Button { onPostponeMinutes(task, 15) } label: {
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") }

View File

@@ -111,6 +111,7 @@ struct TodayView: View {
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -129,6 +130,7 @@ struct TodayView: View {
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -149,6 +151,7 @@ struct TodayView: View {
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -167,6 +170,7 @@ struct TodayView: View {
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -185,6 +189,7 @@ struct TodayView: View {
onEdit: { editingTask = $0 },
onDelete: { taskVM.delete($0) },
onSetDate: { taskVM.setDate($0, date: $1) },
onPostponeMinutes: { taskVM.postpone($0, minutes: $1) },
onMove: { taskVM.moveToQuadrant(taskID: $0.id, quadrant: $1) },
onSetPriority: { taskVM.setPriority($0, priority: $1) },
onSetCategory: { taskVM.setCategory($0, category: $1) },
@@ -540,6 +545,7 @@ struct OverdueCard: View {
var onEdit: (TaskItem) -> Void = { _ in }
var onDelete: (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 }
@@ -642,6 +648,7 @@ struct OverdueCard: View {
task: task,
onPin: onPin,
onSetDate: onSetDate,
onPostponeMinutes: onPostponeMinutes,
onMove: onMove,
onSetPriority: onSetPriority,
onSetCategory: onSetCategory,
@@ -955,6 +962,7 @@ struct UpcomingSection: View {
var onEdit: (TaskItem) -> Void = { _ in }
let onDelete: (TaskItem) -> Void
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 }
@@ -1019,6 +1027,7 @@ struct UpcomingSection: View {
task: task,
onPin: onPin,
onSetDate: onSetDate,
onPostponeMinutes: onPostponeMinutes,
onMove: onMove,
onSetPriority: onSetPriority,
onSetCategory: onSetCategory,
@@ -1930,16 +1939,13 @@ struct TaskDatePickerSheet: View {
@State private var localTime: Date?
@State private var showTimePicker = 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 localReminderTime: Date
@State private var localConstant: Bool
// Custom wheel state (draft committed to the reminder only on "Add")
@State private var showCustomReminder = false
@State private var customNumber: Int
@State private var customUnitWeeks: Bool
@State private var customHour: Int
@State private var customMinute: Int
@State private var customUnit: Int // 0 = minutes, 1 = hours, 2 = days
@State private var localRepeat: String
@State private var localRecurrenceEnd: Date?
@State private var showRecurrenceEndPicker = false
@@ -1972,30 +1978,24 @@ struct TaskDatePickerSheet: View {
_pickerMode = State(initialValue: endDate.wrappedValue != nil ? 1 : 0)
_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 {
let days = cal.dateComponents([.day],
from: cal.startOfDay(for: r),
to: cal.startOfDay(for: start)).day ?? 0
let offset = max(0, days)
_localReminderOffset = State(initialValue: offset)
_localReminderTime = State(initialValue: r)
if offset > 0 && offset % 7 == 0 {
_customUnitWeeks = State(initialValue: true)
_customNumber = State(initialValue: offset / 7)
let base: Date = hasTime.wrappedValue
? (selectedDate.wrappedValue ?? start)
: (cal.date(bySettingHour: 9, minute: 0, second: 0, of: start) ?? start)
let mins = max(0, cal.dateComponents([.minute], from: r, to: base).minute ?? 0)
_localReminderOffset = State(initialValue: mins)
if mins > 0 && mins % 1440 == 0 {
_customUnit = State(initialValue: 2); _customNumber = State(initialValue: mins / 1440)
} else if mins > 0 && mins % 60 == 0 {
_customUnit = State(initialValue: 1); _customNumber = State(initialValue: mins / 60)
} else {
_customUnitWeeks = State(initialValue: false)
_customNumber = State(initialValue: offset)
_customUnit = State(initialValue: 0); _customNumber = State(initialValue: max(1, mins))
}
_customHour = State(initialValue: cal.component(.hour, from: r))
_customMinute = State(initialValue: cal.component(.minute, from: r))
} else {
_localReminderOffset = State(initialValue: nil)
_localReminderTime = State(initialValue: cal.date(bySettingHour: 9, minute: 0, second: 0, of: start) ?? start)
_customUnitWeeks = State(initialValue: false)
_customNumber = State(initialValue: 0)
_customHour = State(initialValue: 9)
_customMinute = State(initialValue: 0)
_customUnit = State(initialValue: 1) // default Custom draft: 1 hour
_customNumber = State(initialValue: 1)
}
}
@@ -2158,14 +2158,15 @@ struct TaskDatePickerSheet: View {
let label: String
}
// Offsets are MINUTES before the event time.
private var reminderPresets: [ReminderPreset] {
[
ReminderPreset(offset: nil, label: "None"),
ReminderPreset(offset: 0, label: "On the day"),
ReminderPreset(offset: 1, label: "1 day early"),
ReminderPreset(offset: 2, label: "2 days early"),
ReminderPreset(offset: 3, label: "3 days early"),
ReminderPreset(offset: 7, label: "1 week early"),
ReminderPreset(offset: nil, label: "None"),
ReminderPreset(offset: 0, label: "On time"),
ReminderPreset(offset: 5, label: "5 minutes early"),
ReminderPreset(offset: 30, label: "30 minutes early"),
ReminderPreset(offset: 60, label: "1 hour early"),
ReminderPreset(offset: 1440, label: "1 day early"),
]
}
@@ -2176,12 +2177,7 @@ struct TaskDatePickerSheet: View {
Button {
withAnimation(KisaniSpring.micro) {
localReminderOffset = preset.offset
if let off = preset.offset {
if off > 0 && off % 7 == 0 { customUnitWeeks = true; customNumber = off / 7 }
else { customUnitWeeks = false; customNumber = off }
} else {
localConstant = false
}
if preset.offset == nil { localConstant = false }
showCustomReminder = false
}
} label: {
@@ -2189,11 +2185,6 @@ struct TaskDatePickerSheet: View {
Text(preset.label)
.font(AppFonts.sans(15))
.foregroundColor(preset.offset == nil ? AppColors.accent : .primary)
if preset.offset != nil {
Text("(\(timeFmt.string(from: localReminderTime)))")
.font(AppFonts.sans(13))
.foregroundColor(.secondary)
}
Spacer()
if !showCustomReminder && localReminderOffset == preset.offset {
Image(systemName: "checkmark")
@@ -2242,38 +2233,34 @@ struct TaskDatePickerSheet: View {
// 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() {
let cal = Calendar.current
if let off = localReminderOffset {
if off > 0 && off % 7 == 0 { customUnitWeeks = true; customNumber = off / 7 }
else { customUnitWeeks = false; customNumber = off }
}
customHour = cal.component(.hour, from: localReminderTime)
customMinute = cal.component(.minute, from: localReminderTime)
guard let off = localReminderOffset, off > 0 else { return }
if off % 1440 == 0 { customUnit = 2; customNumber = off / 1440 }
else if off % 60 == 0 { customUnit = 1; customNumber = off / 60 }
else { customUnit = 0; customNumber = off }
}
private var customOffsetMinutes: Int { max(1, customNumber) * customUnitMinutes[customUnit] }
private func commitCustomReminder() {
let cal = Calendar.current
localReminderOffset = customUnitWeeks ? customNumber * 7 : customNumber
localReminderTime = cal.date(bySettingHour: customHour, minute: customMinute,
second: 0, of: localDate) ?? localReminderTime
localReminderOffset = customOffsetMinutes
}
// 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 {
if n == 0 { return "On the day" }
let unit = customUnitWeeks ? "week" : "day"
let unit = customUnitNames[customUnit]
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.
private var draftReminderDate: Date? {
let cal = Calendar.current
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)
Calendar.current.date(byAdding: .minute, value: -customOffsetMinutes, to: reminderBaseDate())
}
private var draftReminderInPast: Bool {
@@ -2289,42 +2276,30 @@ struct TaskDatePickerSheet: View {
.onTapGesture { withAnimation(KisaniSpring.snappy) { showCustomReminder = false } }
VStack(spacing: 18) {
// Day / Week segmented toggle
// Minutes / Hours / Days segmented toggle
HStack(spacing: 2) {
customModeButton("Day", weeks: false)
customModeButton("Week", weeks: true)
customModeButton("Minutes", unit: 0)
customModeButton("Hours", unit: 1)
customModeButton("Days", unit: 2)
}
.padding(3)
.background(Color(uiColor: .tertiarySystemFill))
.clipShape(Capsule())
.padding(.top, 20)
// Three-column wheel: lead-time · hour · minute
HStack(spacing: 0) {
Picker("", selection: $customNumber) {
ForEach(0...customMaxCount, id: \.self) { n in
Text(customLeadLabel(n)).tag(n)
}
// Single lead-time wheel (relative to the event time)
Picker("", selection: $customNumber) {
ForEach(1...customMaxCount, id: \.self) { n in
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)
.onChange(of: customUnitWeeks) { _ in
.clipped()
.onChange(of: customUnit) { _ in
if customNumber > customMaxCount { customNumber = customMaxCount }
if customNumber < 1 { customNumber = 1 }
}
// Subtle (non-red) note never alarming.
@@ -2371,15 +2346,19 @@ struct TaskDatePickerSheet: View {
}
@ViewBuilder
private func customModeButton(_ label: String, weeks: Bool) -> some View {
private func customModeButton(_ label: String, unit: Int) -> some View {
Button {
withAnimation(KisaniSpring.micro) { customUnitWeeks = weeks }
withAnimation(KisaniSpring.micro) {
customUnit = unit
if customNumber > customMaxCount { customNumber = customMaxCount }
if customNumber < 1 { customNumber = 1 }
}
} label: {
Text(label)
.font(AppFonts.sans(15, weight: customUnitWeeks == weeks ? .semibold : .regular))
.foregroundColor(customUnitWeeks == weeks ? .primary : .secondary)
.frame(width: 96, height: 36)
.background(customUnitWeeks == weeks ? Color(uiColor: .systemGray4) : Color.clear)
.font(AppFonts.sans(15, weight: customUnit == unit ? .semibold : .regular))
.foregroundColor(customUnit == unit ? .primary : .secondary)
.frame(maxWidth: .infinity).frame(height: 36)
.background(customUnit == unit ? Color(uiColor: .systemGray4) : Color.clear)
.clipShape(Capsule())
}
.buttonStyle(.plain)
@@ -2390,22 +2369,26 @@ struct TaskDatePickerSheet: View {
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 base = cal.date(byAdding: .day, value: -offset, to: localDate) ?? localDate
let tc = cal.dateComponents([.hour, .minute], from: localReminderTime)
return cal.date(bySettingHour: tc.hour ?? 9, minute: tc.minute ?? 0, second: 0, of: base)
if let t = localTime {
let tc = cal.dateComponents([.hour, .minute], from: t)
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 {
guard let off = localReminderOffset else { return "None" }
let timeStr = timeFmt.string(from: localReminderTime)
if off == 0 { return "On the day · \(timeStr)" }
if off % 7 == 0 {
let w = off / 7
return "\(w) week\(w == 1 ? "" : "s") early · \(timeStr)"
}
return "\(off) day\(off == 1 ? "" : "s") early · \(timeStr)"
if off == 0 { return "On time" }
if off % 1440 == 0 { let d = off / 1440; return "\(d) day\(d == 1 ? "" : "s") early" }
if off % 60 == 0 { let h = off / 60; return "\(h) hour\(h == 1 ? "" : "s") early" }
return "\(off) minute\(off == 1 ? "" : "s") early"
}
@ViewBuilder