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:
@@ -81,7 +81,7 @@
|
||||
0506183945D16EC443A69651 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
|
||||
0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskLiveActivity.swift; sourceTree = "<group>"; };
|
||||
0C96EC00F6B021DBA3CC1A79 /* ProfileSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSetupView.swift; sourceTree = "<group>"; };
|
||||
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0D6D6740721F01A74E404EB9 /* KisaniCalWidgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = KisaniCalWidgets.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
106EEF572C6F8990408329F0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
|
||||
1AA7498EFED7692022F3E7E1 /* KisaniCal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = KisaniCal.entitlements; sourceTree = "<group>"; };
|
||||
208F82348DEBD9FF7B0DCF17 /* MyTasksWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyTasksWidget.swift; sourceTree = "<group>"; };
|
||||
@@ -105,7 +105,7 @@
|
||||
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
|
||||
92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; };
|
||||
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
|
||||
AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
|
||||
BC5E179A9189B0A8C3F856F6 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
@@ -292,7 +292,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1500;
|
||||
LastUpgradeCheck = 2620;
|
||||
TargetAttributes = {
|
||||
28B43516AD88946E21D9BFE0 = {
|
||||
DevelopmentTeam = K8BLMMR883;
|
||||
@@ -409,6 +409,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Wenza;
|
||||
@@ -420,13 +421,14 @@
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -473,6 +475,7 @@
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
@@ -487,6 +490,7 @@
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -499,10 +503,16 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -549,6 +559,7 @@
|
||||
DEVELOPMENT_TEAM = K8BLMMR883;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
@@ -570,6 +581,7 @@
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -582,10 +594,16 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCalWidgets/KisaniCalWidgets.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = KisaniCalWidgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal.KisaniCalWidgets;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
@@ -601,6 +619,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = KisaniCal/KisaniCal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Wenza;
|
||||
@@ -612,13 +631,14 @@
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.kutesir.KisaniCal;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.9;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1500"
|
||||
version = "1.7">
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
runPostActionsOnFailure = "NO">
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -27,8 +26,7 @@
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
@@ -40,8 +38,6 @@
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
</Testables>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
@@ -63,8 +59,6 @@
|
||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@@ -82,8 +76,6 @@
|
||||
ReferencedContainer = "container:KisaniCal.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
</CommandLineArguments>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -238,11 +238,26 @@ struct EventProvider: AppIntentTimelineProvider {
|
||||
isRecurring: $0.isRecurring, recurrenceLabel: $0.recurrenceLabel) }
|
||||
}
|
||||
|
||||
/// Due date of the event the auto-select queue is currently showing (nil if not in
|
||||
/// queue mode, an event is tracked, or there's nothing upcoming).
|
||||
/// The tracked task ("Track Countdown"), but only while it's still live: not
|
||||
/// completed, and either recurring or with a deadline still ahead of `now`.
|
||||
/// Returns nil once it's done/expired so the widget advances to the next event.
|
||||
private func trackedTask(asOf now: Date) -> WidgetTask? {
|
||||
guard let trackedId = UserDefaults.kisani.string(forKey: "kisani.widget.trackedEvent"),
|
||||
let task = loadWidgetTasks().first(where: { $0.id.uuidString == trackedId }),
|
||||
let due = task.dueDate,
|
||||
!task.isComplete,
|
||||
task.isRecurring || due > now
|
||||
else { return nil }
|
||||
return task
|
||||
}
|
||||
|
||||
/// Due date the currently-shown event expires at, so the timeline can refresh
|
||||
/// the moment it's done. Covers both the tracked event and the auto-select queue.
|
||||
private func currentSelectionExpiry(for config: EventConfigIntent, asOf now: Date) -> Date? {
|
||||
guard config.upcomingQueue,
|
||||
UserDefaults.kisani.string(forKey: "kisani.widget.trackedEvent") == nil else { return nil }
|
||||
if let task = trackedTask(asOf: now) {
|
||||
return task.isRecurring ? nil : task.dueDate
|
||||
}
|
||||
guard config.upcomingQueue else { return nil }
|
||||
let upcoming = upcomingEvents(asOf: now)
|
||||
let idx = max(0, config.autoSelect.rawValue - 1)
|
||||
guard idx < upcoming.count else { return nil }
|
||||
@@ -266,14 +281,14 @@ struct EventProvider: AppIntentTimelineProvider {
|
||||
let mode = UserDefaults.kisani.integer(forKey: countdownUnitKey)
|
||||
|
||||
// An event tracked from the app ("Track Countdown") bypasses the widget
|
||||
// configuration entirely — title, date, and progress come straight from
|
||||
// App Group storage.
|
||||
if let trackedId = UserDefaults.kisani.string(forKey: "kisani.widget.trackedEvent"),
|
||||
let task = loadWidgetTasks().first(where: { $0.id.uuidString == trackedId }),
|
||||
let due = task.dueDate {
|
||||
// configuration entirely. Once it's completed or its deadline has passed
|
||||
// (and it isn't recurring), stop pinning it and fall through to the
|
||||
// next-nearest event below — so the widget always advances.
|
||||
if let task = trackedTask(asOf: now) {
|
||||
let trackedId = task.id.uuidString
|
||||
let since = UserDefaults.kisani.object(forKey: "kisani.widget.trackedSince") as? Date
|
||||
let s = resolveSpan(due: due, isRecurring: task.isRecurring, label: task.recurrenceLabel,
|
||||
anchorKey: trackedId, explicitStart: since)
|
||||
let s = resolveSpan(due: task.dueDate ?? now, isRecurring: task.isRecurring,
|
||||
label: task.recurrenceLabel, anchorKey: trackedId, explicitStart: since)
|
||||
return EventEntry(date: now, eventName: task.title, start: s.start, target: s.target, unitMode: mode)
|
||||
}
|
||||
|
||||
|
||||
@@ -77,25 +77,33 @@ struct ProgressDotView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var isAccessory: Bool { family == .accessoryRectangular }
|
||||
|
||||
// 100 tiny dots are illegible in a lock-screen pill — show a coarse grid there.
|
||||
private var dotCount: Int { isAccessory ? 28 : entry.totalDots }
|
||||
|
||||
var body: some View {
|
||||
let percent = Int((entry.progress * 100).rounded(.down))
|
||||
VStack(alignment: .leading, spacing: spacing) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(entry.title)
|
||||
.font(headerFont)
|
||||
.foregroundColor(.white)
|
||||
.foregroundColor(isAccessory ? .primary : .white)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.72)
|
||||
Spacer()
|
||||
Text("\(percent)%")
|
||||
.font(headerFont)
|
||||
.foregroundColor(Color(white: 0.5))
|
||||
.foregroundColor(isAccessory ? .primary.opacity(0.7) : Color(white: 0.5))
|
||||
.lineLimit(1)
|
||||
}
|
||||
DotGridView(progress: entry.progress, totalDots: entry.totalDots, color: accent)
|
||||
DotGridView(progress: entry.progress, totalDots: dotCount,
|
||||
color: isAccessory ? .primary : accent)
|
||||
}
|
||||
.padding(.horizontal, horizontalPadding)
|
||||
.containerBackground(WidgetTheme.background, for: .widget)
|
||||
.containerBackground(for: .widget) {
|
||||
if isAccessory { AccessoryWidgetBackground() } else { WidgetTheme.background }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user