Today view: reorder sections + keep RSS feeds out of tasks
- Move the Overdue card to sit directly below today's active tasks (above Next 3 Days) so your real tasks stay at the top. - Filter subscription/RSS calendar feeds (F1, TV shows, etc.) out of the Today/Tasks timeline; they remain in the Calendar tab. Birthdays and personal calendars are unaffected. Note: also carries prior in-progress TodayView edits that were already in the working tree before this session. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,10 @@ struct TodayView: View {
|
|||||||
|
|
||||||
private var todayCalEvents: [EKEvent] {
|
private var todayCalEvents: [EKEvent] {
|
||||||
calStore.monthEvents.filter {
|
calStore.monthEvents.filter {
|
||||||
Calendar.current.isDateInToday($0.startDate ?? .distantFuture)
|
// Subscribed/RSS feed calendars (F1 schedule, TV shows, etc.) belong in the
|
||||||
|
// Calendar tab only — keep them out of the Today/Tasks timeline.
|
||||||
|
$0.calendar?.type != .subscription
|
||||||
|
&& Calendar.current.isDateInToday($0.startDate ?? .distantFuture)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,21 +81,6 @@ struct TodayView: View {
|
|||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Overdue card (prominent, above timeline) ──
|
|
||||||
if !taskVM.overdueTasks.isEmpty {
|
|
||||||
OverdueCard(
|
|
||||||
tasks: taskVM.overdueTasks,
|
|
||||||
onPostpone: { taskVM.postponeAllOverdue() },
|
|
||||||
onToggle: { task in withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } },
|
|
||||||
onPostponeTask: { taskVM.postpone($0) },
|
|
||||||
onPin: { taskVM.pin($0) },
|
|
||||||
onEdit: { editingTask = $0 },
|
|
||||||
onDelete: { taskVM.delete($0) }
|
|
||||||
)
|
|
||||||
.padding(.horizontal, 18)
|
|
||||||
.padding(.bottom, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Today timeline: active → completed (green) ──
|
// ── Today timeline: active → completed (green) ──
|
||||||
let activeTasks = taskVM.todayTasks
|
let activeTasks = taskVM.todayTasks
|
||||||
let completedTasks = hideCompleted ? [] : taskVM.completedTodayTasks
|
let completedTasks = hideCompleted ? [] : taskVM.completedTodayTasks
|
||||||
@@ -116,6 +104,21 @@ struct TodayView: View {
|
|||||||
)
|
)
|
||||||
.padding(.horizontal, 18)
|
.padding(.horizontal, 18)
|
||||||
|
|
||||||
|
// ── Overdue (below Today's tasks, above Next 3 Days) ──
|
||||||
|
if !taskVM.overdueTasks.isEmpty {
|
||||||
|
OverdueCard(
|
||||||
|
tasks: taskVM.overdueTasks,
|
||||||
|
onPostpone: { taskVM.postponeAllOverdue() },
|
||||||
|
onToggle: { task in withAnimation(KisaniSpring.bounce) { taskVM.toggle(task) } },
|
||||||
|
onPostponeTask: { taskVM.postpone($0) },
|
||||||
|
onPin: { taskVM.pin($0) },
|
||||||
|
onEdit: { editingTask = $0 },
|
||||||
|
onDelete: { taskVM.delete($0) }
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.padding(.top, 10)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Next 3 Days ──
|
// ── Next 3 Days ──
|
||||||
let next3 = taskVM.next3DaysTasks
|
let next3 = taskVM.next3DaysTasks
|
||||||
if !next3.isEmpty {
|
if !next3.isEmpty {
|
||||||
@@ -1635,6 +1638,7 @@ struct AddTaskSheet: View {
|
|||||||
@State private var selectedQuadrant: Quadrant
|
@State private var selectedQuadrant: Quadrant
|
||||||
@State private var selectedCategory: TaskCategory = .reminder
|
@State private var selectedCategory: TaskCategory = .reminder
|
||||||
@State private var reminderDate: Date? = nil
|
@State private var reminderDate: Date? = nil
|
||||||
|
@State private var constantReminder = false
|
||||||
@State private var showDatePicker = false
|
@State private var showDatePicker = false
|
||||||
|
|
||||||
var prefilledDate: Date?
|
var prefilledDate: Date?
|
||||||
@@ -1797,7 +1801,8 @@ struct AddTaskSheet: View {
|
|||||||
recurrenceLabel: $parsed.recurrenceLabel,
|
recurrenceLabel: $parsed.recurrenceLabel,
|
||||||
endDate: $parsed.endDate,
|
endDate: $parsed.endDate,
|
||||||
isAllDay: $parsed.isAllDay,
|
isAllDay: $parsed.isAllDay,
|
||||||
reminderDate: $reminderDate
|
reminderDate: $reminderDate,
|
||||||
|
constantReminder: $constantReminder
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1808,7 +1813,7 @@ struct AddTaskSheet: View {
|
|||||||
quadrant: selectedQuadrant, category: selectedCategory,
|
quadrant: selectedQuadrant, category: selectedCategory,
|
||||||
isRecurring: parsed.isRecurring, recurrenceLabel: parsed.recurrenceLabel,
|
isRecurring: parsed.isRecurring, recurrenceLabel: parsed.recurrenceLabel,
|
||||||
endDate: parsed.endDate, isAllDay: parsed.isAllDay,
|
endDate: parsed.endDate, isAllDay: parsed.isAllDay,
|
||||||
reminderDate: reminderDate)
|
reminderDate: reminderDate, constantReminder: constantReminder)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,6 +1850,7 @@ struct TaskDatePickerSheet: View {
|
|||||||
@Binding var endDate: Date?
|
@Binding var endDate: Date?
|
||||||
@Binding var isAllDay: Bool
|
@Binding var isAllDay: Bool
|
||||||
@Binding var reminderDate: Date?
|
@Binding var reminderDate: Date?
|
||||||
|
@Binding var constantReminder: Bool
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@Environment(\.colorScheme) private var cs
|
@Environment(\.colorScheme) private var cs
|
||||||
|
|
||||||
@@ -1853,8 +1859,17 @@ struct TaskDatePickerSheet: View {
|
|||||||
@State private var localEndDate: Date
|
@State private var localEndDate: Date
|
||||||
@State private var localTime: Date?
|
@State private var localTime: Date?
|
||||||
@State private var showTimePicker = false
|
@State private var showTimePicker = false
|
||||||
@State private var localReminder: Date?
|
|
||||||
@State private var showReminderPicker = false
|
@State private var showReminderPicker = false
|
||||||
|
// Reminder lead-time, expressed as days before the task date (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 localRepeat: String
|
@State private var localRepeat: String
|
||||||
@State private var localIsAllDay: Bool
|
@State private var localIsAllDay: Bool
|
||||||
@State private var editingEnd = false
|
@State private var editingEnd = false
|
||||||
@@ -1862,22 +1877,51 @@ struct TaskDatePickerSheet: View {
|
|||||||
init(selectedDate: Binding<Date?>, hasTime: Binding<Bool>,
|
init(selectedDate: Binding<Date?>, hasTime: Binding<Bool>,
|
||||||
isRecurring: Binding<Bool>, recurrenceLabel: Binding<String?>,
|
isRecurring: Binding<Bool>, recurrenceLabel: Binding<String?>,
|
||||||
endDate: Binding<Date?>, isAllDay: Binding<Bool>,
|
endDate: Binding<Date?>, isAllDay: Binding<Bool>,
|
||||||
reminderDate: Binding<Date?> = .constant(nil)) {
|
reminderDate: Binding<Date?> = .constant(nil),
|
||||||
_selectedDate = selectedDate
|
constantReminder: Binding<Bool> = .constant(false)) {
|
||||||
_hasTime = hasTime
|
_selectedDate = selectedDate
|
||||||
_isRecurring = isRecurring
|
_hasTime = hasTime
|
||||||
_recurrenceLabel = recurrenceLabel
|
_isRecurring = isRecurring
|
||||||
_endDate = endDate
|
_recurrenceLabel = recurrenceLabel
|
||||||
_isAllDay = isAllDay
|
_endDate = endDate
|
||||||
_reminderDate = reminderDate
|
_isAllDay = isAllDay
|
||||||
let start = selectedDate.wrappedValue ?? Calendar.current.startOfDay(for: Date())
|
_reminderDate = reminderDate
|
||||||
|
_constantReminder = constantReminder
|
||||||
|
let cal = Calendar.current
|
||||||
|
let start = selectedDate.wrappedValue ?? cal.startOfDay(for: Date())
|
||||||
_localDate = State(initialValue: start)
|
_localDate = State(initialValue: start)
|
||||||
_localEndDate = State(initialValue: endDate.wrappedValue ?? start)
|
_localEndDate = State(initialValue: endDate.wrappedValue ?? start)
|
||||||
_localTime = State(initialValue: hasTime.wrappedValue ? selectedDate.wrappedValue : nil)
|
_localTime = State(initialValue: hasTime.wrappedValue ? selectedDate.wrappedValue : nil)
|
||||||
_localReminder = State(initialValue: reminderDate.wrappedValue)
|
|
||||||
_localRepeat = State(initialValue: recurrenceLabel.wrappedValue ?? "None")
|
_localRepeat = State(initialValue: recurrenceLabel.wrappedValue ?? "None")
|
||||||
_localIsAllDay = State(initialValue: isAllDay.wrappedValue)
|
_localIsAllDay = State(initialValue: isAllDay.wrappedValue)
|
||||||
_pickerMode = State(initialValue: endDate.wrappedValue != nil ? 1 : 0)
|
_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.
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
_customUnitWeeks = State(initialValue: false)
|
||||||
|
_customNumber = State(initialValue: offset)
|
||||||
|
}
|
||||||
|
_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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -1936,6 +1980,9 @@ struct TaskDatePickerSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.overlay {
|
||||||
|
if showCustomReminder { customReminderModal }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -1973,30 +2020,14 @@ struct TaskDatePickerSheet: View {
|
|||||||
|
|
||||||
Button { withAnimation(KisaniSpring.micro) { showReminderPicker.toggle(); if showReminderPicker { showTimePicker = false } } } label: {
|
Button { withAnimation(KisaniSpring.micro) { showReminderPicker.toggle(); if showReminderPicker { showTimePicker = false } } } label: {
|
||||||
pickerRow(icon: "alarm", label: "Reminder",
|
pickerRow(icon: "alarm", label: "Reminder",
|
||||||
value: localReminder != nil ? timeFmt.string(from: localReminder!) : "None",
|
value: reminderSummary,
|
||||||
active: localReminder != nil)
|
active: localReminderOffset != nil)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
if showReminderPicker {
|
if showReminderPicker {
|
||||||
DatePicker("", selection: Binding(
|
reminderOptions
|
||||||
get: { localReminder ?? Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: localDate) ?? localDate },
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||||
set: { localReminder = $0 }
|
|
||||||
), displayedComponents: .hourAndMinute)
|
|
||||||
.datePickerStyle(.wheel).labelsHidden()
|
|
||||||
.transition(.opacity.combined(with: .move(edge: .top)))
|
|
||||||
|
|
||||||
Button {
|
|
||||||
withAnimation(KisaniSpring.micro) {
|
|
||||||
localReminder = nil
|
|
||||||
showReminderPicker = false
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("Clear Reminder")
|
|
||||||
.font(AppFonts.sans(13))
|
|
||||||
.foregroundColor(AppColors.accent)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Divider().padding(.leading, 54)
|
Divider().padding(.leading, 54)
|
||||||
@@ -2018,6 +2049,264 @@ struct TaskDatePickerSheet: View {
|
|||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Reminder lead-time picker
|
||||||
|
|
||||||
|
private struct ReminderPreset: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
let offset: Int? // days before; nil = None
|
||||||
|
let label: String
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var reminderOptions: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ForEach(reminderPresets) { preset in
|
||||||
|
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
|
||||||
|
}
|
||||||
|
showCustomReminder = false
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
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")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundColor(AppColors.accent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16).frame(height: 44)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom — opens the Day/Week wheel picker.
|
||||||
|
Button {
|
||||||
|
seedCustomDraft()
|
||||||
|
withAnimation(KisaniSpring.snappy) { showCustomReminder = true }
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text("Custom")
|
||||||
|
.font(AppFonts.sans(15)).foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: 12, weight: .semibold))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16).frame(height: 44)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
// Subtle (non-red) note when the chosen reminder time is already past.
|
||||||
|
if reminderInPast {
|
||||||
|
Text("Selected reminder time has already passed.")
|
||||||
|
.font(AppFonts.sans(12))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 16).padding(.bottom, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant Reminder toggle intentionally hidden — to be gated behind a
|
||||||
|
// paywall later. Model field + scheduler support remain in place.
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Custom reminder wheel (Day / Week)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commitCustomReminder() {
|
||||||
|
let cal = Calendar.current
|
||||||
|
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.
|
||||||
|
private func customLeadLabel(_ n: Int) -> String {
|
||||||
|
if n == 0 { return "On the day" }
|
||||||
|
let unit = customUnitWeeks ? "week" : "day"
|
||||||
|
return "\(n) \(unit)\(n == 1 ? "" : "s") early"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var customMaxCount: Int { customUnitWeeks ? 8 : 60 }
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var draftReminderInPast: Bool {
|
||||||
|
(draftReminderDate ?? Date()) < Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var customReminderModal: some View {
|
||||||
|
ZStack {
|
||||||
|
// Dimmed scrim — tap to cancel.
|
||||||
|
Color.black.opacity(0.45)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onTapGesture { withAnimation(KisaniSpring.snappy) { showCustomReminder = false } }
|
||||||
|
|
||||||
|
VStack(spacing: 18) {
|
||||||
|
// Day / Week segmented toggle
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
customModeButton("Day", weeks: false)
|
||||||
|
customModeButton("Week", weeks: true)
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
.frame(height: 180)
|
||||||
|
.onChange(of: customUnitWeeks) { _ in
|
||||||
|
if customNumber > customMaxCount { customNumber = customMaxCount }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtle (non-red) note — never alarming.
|
||||||
|
if draftReminderInPast {
|
||||||
|
Text("Selected reminder time has already passed.")
|
||||||
|
.font(AppFonts.sans(13))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel / Add
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Button {
|
||||||
|
withAnimation(KisaniSpring.snappy) { showCustomReminder = false }
|
||||||
|
} label: {
|
||||||
|
Text("Cancel")
|
||||||
|
.font(AppFonts.sans(16, weight: .medium))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(maxWidth: .infinity).frame(height: 52)
|
||||||
|
.background(Color(uiColor: .tertiarySystemFill))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
commitCustomReminder()
|
||||||
|
withAnimation(KisaniSpring.snappy) { showCustomReminder = false }
|
||||||
|
} label: {
|
||||||
|
Text("Add")
|
||||||
|
.font(AppFonts.sans(16, weight: .semibold))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(maxWidth: .infinity).frame(height: 52)
|
||||||
|
.background(Color(uiColor: .tertiarySystemFill))
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20).padding(.bottom, 20)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: 380)
|
||||||
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 28))
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: 28).stroke(Color.white.opacity(0.08), lineWidth: 1))
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.transition(.scale(scale: 0.92).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func customModeButton(_ label: String, weeks: Bool) -> some View {
|
||||||
|
Button {
|
||||||
|
withAnimation(KisaniSpring.micro) { customUnitWeeks = weeks }
|
||||||
|
} 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)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var reminderInPast: Bool {
|
||||||
|
guard let off = localReminderOffset else { return false }
|
||||||
|
return computedReminderDate(offset: off) ?? Date() < Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func computedReminderDate(offset: Int) -> 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)"
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var durationContent: some View {
|
private var durationContent: some View {
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
@@ -2175,21 +2464,21 @@ struct TaskDatePickerSheet: View {
|
|||||||
isAllDay = false
|
isAllDay = false
|
||||||
isRecurring = localRepeat != "None"
|
isRecurring = localRepeat != "None"
|
||||||
recurrenceLabel = localRepeat != "None" ? localRepeat : nil
|
recurrenceLabel = localRepeat != "None" ? localRepeat : nil
|
||||||
// Combine reminder time with the selected date
|
// Reminder is a lead-time offset from the task date at the chosen time-of-day.
|
||||||
if let r = localReminder {
|
if let off = localReminderOffset {
|
||||||
let rc = Calendar.current.dateComponents([.hour, .minute], from: r)
|
reminderDate = computedReminderDate(offset: off)
|
||||||
reminderDate = Calendar.current.date(bySettingHour: rc.hour ?? 9,
|
constantReminder = localConstant
|
||||||
minute: rc.minute ?? 0,
|
|
||||||
second: 0, of: localDate)
|
|
||||||
} else {
|
} else {
|
||||||
reminderDate = nil
|
reminderDate = nil
|
||||||
|
constantReminder = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
selectedDate = Calendar.current.startOfDay(for: localDate)
|
selectedDate = Calendar.current.startOfDay(for: localDate)
|
||||||
endDate = Calendar.current.startOfDay(for: localEndDate)
|
endDate = Calendar.current.startOfDay(for: localEndDate)
|
||||||
isAllDay = localIsAllDay
|
isAllDay = localIsAllDay
|
||||||
hasTime = false
|
hasTime = false
|
||||||
reminderDate = nil
|
reminderDate = nil
|
||||||
|
constantReminder = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user