Widgets/notifications: tracked-event auto-advance, lock-screen dots, Snooze labels, period milestones

- Bars widget: tracked ("Track Countdown") events now also advance — a completed
  or past (non-recurring) tracked event falls through to the next-nearest, and the
  refresh boundary covers the tracked event's expiry. Recurring tracked events
  still roll forward. Completes the KC-26 auto-advance for all paths.
- Progress Dots on the lock screen (accessoryRectangular): show 28 larger dots
  instead of 100 tiny ones, render with .primary for the vibrant tint, and use
  AccessoryWidgetBackground so it's legible (Home Screen keeps the 100-dot grid).
- Notifications: snooze action labels now read "Snooze 15 min/30 min/1 hour/
  2 hours" (lock screen) and "Snooze 15 Minutes…" (in-app Postpone menu).
- New period milestone notifications: recurring "One more week/month/year passed"
  at end of week (Sun 20:00), month (1st 09:00), and year (Jan 1 09:00); gated by
  a kisani.periodMilestones flag, scheduled via the existing reschedule path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-16 02:12:52 +03:00
parent 87c999c676
commit 48e2f4fc10
7 changed files with 122 additions and 52 deletions

View File

@@ -722,10 +722,10 @@ 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
- Snooze 15 Minutes
- Snooze 30 Minutes
- Snooze 1 Hour
- Snooze 2 Hours
- Tomorrow
- Wired the submenu through:
- Today timeline + overdue/upcoming sections
@@ -738,10 +738,10 @@ snooze choices directly from the pressed item and notification actions.
### Fix (lock screen notifications)
- Task reminder notifications now include actions:
- 15 min
- 30 min
- 1 hour
- 2 hours
- Snooze 15 min
- Snooze 30 min
- Snooze 1 hour
- Snooze 2 hours
- Notification response handler updates the stored task reminder and schedules
a one-off snoozed notification for the selected delay.

View File

@@ -30,10 +30,10 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
private func registerCategories() {
let markAction = UNNotificationAction(identifier: Self.actionMarkId, title: "Mark Complete", options: [])
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: [])
UNNotificationAction(identifier: Self.taskSnoozePrefix + "15", title: "Snooze 15 min", options: []),
UNNotificationAction(identifier: Self.taskSnoozePrefix + "30", title: "Snooze 30 min", options: []),
UNNotificationAction(identifier: Self.taskSnoozePrefix + "60", title: "Snooze 1 hour", options: []),
UNNotificationAction(identifier: Self.taskSnoozePrefix + "120", title: "Snooze 2 hours", options: [])
]
let taskCat = UNNotificationCategory(identifier: Self.categoryId, actions: [markAction] + taskSnoozeActions,
intentIdentifiers: [], options: .customDismissAction)
@@ -129,10 +129,45 @@ final class NotificationManager: NSObject, ObservableObject, @unchecked Sendable
self.scheduleCompensations(compensations, programs: programs)
self.scheduleTasks(snapshot: tasks)
self.scheduleCalendarEvents(snapshot: calEvents)
self.schedulePeriodMilestones()
self.updateBadge(snapshot: tasks)
}
}
// MARK: - Period milestones ("one more week / month / year passed")
private static let periodIds = ["kisani.period.week", "kisani.period.month", "kisani.period.year"]
/// Recurring nudges at the close of each week, month, and year. Fixed identifiers
/// + remove-then-add keep them de-duplicated across reschedules. Disabled (and
/// cleared) when the user turns off the "Period milestones" setting.
private func schedulePeriodMilestones() {
center.removePendingNotificationRequests(withIdentifiers: Self.periodIds)
guard UserDefaults.kisani.object(forKey: "kisani.periodMilestones") as? Bool ?? true else { return }
func add(_ id: String, _ title: String, _ body: String, _ comps: DateComponents) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
center.add(UNNotificationRequest(
identifier: id, content: content,
trigger: UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)))
}
// End of week Sunday 20:00
var week = DateComponents(); week.weekday = 1; week.hour = 20; week.minute = 0
add(Self.periodIds[0], "This Week", "One more week passed.", week)
// End of month 1st of the next month, 09:00
var month = DateComponents(); month.day = 1; month.hour = 9; month.minute = 0
add(Self.periodIds[1], "This Month", "One more month passed.", month)
// End of year Jan 1, 09:00
var year = DateComponents(); year.month = 1; year.day = 1; year.hour = 9; year.minute = 0
add(Self.periodIds[2], "This Year", "One more year passed.", year)
}
// MARK: - Compensation workout reminders (one-off, on chosen rest days)
private func scheduleCompensations(_ comps: [String: String], programs: [WorkoutProgram]) {

View File

@@ -71,16 +71,16 @@ struct TaskMenuItems: View {
Menu {
Button { onPostponeMinutes(task, 15) } label: {
Label("15 Minutes", systemImage: "clock")
Label("Snooze 15 Minutes", systemImage: "clock")
}
Button { onPostponeMinutes(task, 30) } label: {
Label("30 Minutes", systemImage: "clock")
Label("Snooze 30 Minutes", systemImage: "clock")
}
Button { onPostponeMinutes(task, 60) } label: {
Label("1 Hour", systemImage: "clock")
Label("Snooze 1 Hour", systemImage: "clock")
}
Button { onPostponeMinutes(task, 120) } label: {
Label("2 Hours", systemImage: "clock")
Label("Snooze 2 Hours", systemImage: "clock")
}
Divider()
Button { onSetDate(task, preservingTime(on: dayOffset(1))) } label: {