diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index ed699f5..4a4e289 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -474,7 +474,7 @@ struct ContentView: View { GeometryReader { geo in MarqueeText( .constant(reminderSneakPeekText(for: reminder)), - textColor: reminderColor(for: reminder.event), + textColor: reminderColor(for: reminder, now: reminderManager.currentDate), minDuration: 1, frameWidth: geo.size.width ) @@ -522,8 +522,13 @@ struct ContentView: View { } } - private func reminderColor(for event: EventModel) -> Color { - Color(nsColor: event.calendar.color).ensureMinimumBrightness(factor: 0.7) + private func reminderColor(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> Color { + let window = TimeInterval(Defaults[.reminderSneakPeekDuration]) + let remaining = reminder.event.start.timeIntervalSince(now) + if window > 0 && remaining > 0 && remaining <= window { + return .red + } + return Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7) } private func reminderSneakPeekText(for entry: ReminderLiveActivityManager.ReminderEntry) -> String { diff --git a/DynamicIsland/components/Calendar/ReminderLiveActivity.swift b/DynamicIsland/components/Calendar/ReminderLiveActivity.swift index 5275a34..f2be48d 100644 --- a/DynamicIsland/components/Calendar/ReminderLiveActivity.swift +++ b/DynamicIsland/components/Calendar/ReminderLiveActivity.swift @@ -42,7 +42,7 @@ struct ReminderLiveActivity: View { Color.clear .frame(width: leftWingWidth, height: notchContentHeight) .background(alignment: .leading) { - iconSection(for: reminder) + iconSection(for: reminder, now: now) .padding(.leading, wingPadding / 2) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } @@ -62,11 +62,11 @@ struct ReminderLiveActivity: View { .frame(height: notchContentHeight, alignment: .center) } - private func iconSection(for reminder: ReminderLiveActivityManager.ReminderEntry) -> some View { + private func iconSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View { let diameter = iconDiameter - let accent = accentColor(for: reminder) + let accent = accentColor(for: reminder, now: now) - return Image(systemName: glyphName(for: reminder.event)) + return Image(systemName: iconName(for: reminder, now: now)) .font(.system(size: 16, weight: .semibold)) .foregroundStyle(accent) .frame(width: diameter, height: diameter) @@ -75,49 +75,49 @@ struct ReminderLiveActivity: View { @ViewBuilder private func rightSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View { - let accent = accentColor(for: reminder) + let accent = accentColor(for: reminder, now: now) switch presentationStyle { case .ringCountdown: ringCountdownSection(for: reminder, now: now, accent: accent) case .digital: - digitalSection(for: reminder, now: now) + digitalSection(for: reminder, now: now, accent: accent) case .minutes: - minutesSection(for: reminder, now: now) + minutesSection(for: reminder, now: now, accent: accent) } } private func ringCountdownSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View { - let progress = progress(for: reminder, now: now) + let progressValue = progress(for: reminder, now: now) let diameter = ringDiameter return ZStack { Circle() .stroke(Color.white.opacity(0.15), lineWidth: ringStrokeWidth) Circle() - .trim(from: 0, to: progress) + .trim(from: 0, to: progressValue) .stroke(accent, style: StrokeStyle(lineWidth: ringStrokeWidth, lineCap: .round)) .rotationEffect(.degrees(-90)) - .animation(.smooth(duration: 0.25), value: progress) + .animation(.smooth(duration: 0.25), value: progressValue) } .frame(width: diameter, height: diameter) .frame(height: notchContentHeight, alignment: .center) } - private func digitalSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View { + private func digitalSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View { let countdown = digitalCountdown(for: reminder, now: now) return Text(countdown) .font(.system(size: 16, weight: .semibold, design: .monospaced)) - .foregroundColor(.white) + .foregroundColor(accent) .contentTransition(.numericText()) .animation(.smooth(duration: 0.25), value: countdown) .frame(height: notchContentHeight, alignment: .center) } - private func minutesSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> some View { + private func minutesSection(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date, accent: Color) -> some View { VStack(alignment: .trailing, spacing: 2) { Text(minutesCountdown(for: reminder, now: now)) .font(.system(size: 13, weight: .semibold)) - .foregroundColor(.white) + .foregroundColor(accent) } .frame(height: notchContentHeight, alignment: .center) } @@ -144,23 +144,15 @@ struct ReminderLiveActivity: View { return minutes == 1 ? "in 1 min" : "in \(minutes) min" } - private func glyphName(for event: EventModel) -> String { - switch event.type { - case .birthday: - return "gift.fill" - case .reminder(let completed): - return completed ? "checkmark.circle" : "bell.fill" - case .event: - return event.isMeeting ? "person.2.fill" : "calendar" + private func accentColor(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> Color { + if isCritical(for: reminder, now: now) { + return .red } - } - - private func accentColor(for reminder: ReminderLiveActivityManager.ReminderEntry) -> Color { - Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7) + return Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7) } private var leftWingWidth: CGFloat { - wingPadding + iconDiameter + wingPadding + iconDiameter } private func rightWingWidth(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> CGFloat { @@ -216,4 +208,15 @@ struct ReminderLiveActivity: View { return UIFont.monospacedDigitSystemFont(ofSize: size, weight: weight) #endif } + + private func iconName(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> String { + isCritical(for: reminder, now: now) ? ReminderLiveActivityManager.criticalIconName : ReminderLiveActivityManager.standardIconName + } + + private func isCritical(for reminder: ReminderLiveActivityManager.ReminderEntry, now: Date) -> Bool { + let window = TimeInterval(Defaults[.reminderSneakPeekDuration]) + guard window > 0 else { return false } + let remaining = reminder.event.start.timeIntervalSince(now) + return remaining > 0 && remaining <= window + } } diff --git a/DynamicIsland/managers/ReminderLiveActivityManager.swift b/DynamicIsland/managers/ReminderLiveActivityManager.swift index d31f1bd..1ea0c66 100644 --- a/DynamicIsland/managers/ReminderLiveActivityManager.swift +++ b/DynamicIsland/managers/ReminderLiveActivityManager.swift @@ -19,6 +19,8 @@ final class ReminderLiveActivityManager: ObservableObject { } static let shared = ReminderLiveActivityManager() + static let standardIconName = "calendar.badge.clock" + static let criticalIconName = "calendar.badge.exclamationmark" @Published private(set) var activeReminder: ReminderEntry? @Published private(set) var currentDate: Date = Date() @@ -36,6 +38,7 @@ final class ReminderLiveActivityManager: ObservableObject { private let minimumRefreshInterval: TimeInterval = 10 private let refreshDebounceInterval: TimeInterval = 0.3 private var refreshTaskToken = UUID() + private var hasShownCriticalSneakPeek = false private let calendarService: CalendarServiceProviding private let calendarManager = CalendarManager.shared @@ -110,6 +113,7 @@ final class ReminderLiveActivityManager: ObservableObject { pendingRefreshForce = false refreshTask?.cancel() refreshTask = nil + hasShownCriticalSneakPeek = false } private func deactivateReminder() { @@ -254,6 +258,7 @@ final class ReminderLiveActivityManager: ObservableObject { private func handleEntrySelection(_ entry: ReminderEntry?, referenceDate: Date) { fallbackRefreshTask?.cancel() nextReminder = entry + hasShownCriticalSneakPeek = false Task { await self.evaluateCurrentState(at: referenceDate) } } @@ -317,6 +322,7 @@ final class ReminderLiveActivityManager: ObservableObject { activeReminder = nil nextReminder = nil stopTicker() + hasShownCriticalSneakPeek = false scheduleRefresh(force: true) return } @@ -333,8 +339,28 @@ final class ReminderLiveActivityManager: ObservableObject { type: .reminder, duration: Defaults[.reminderSneakPeekDuration], value: 0, - icon: glyphName(for: entry.event) + icon: ReminderLiveActivityManager.standardIconName ) + hasShownCriticalSneakPeek = false + } + + let criticalWindow = TimeInterval(Defaults[.reminderSneakPeekDuration]) + let timeRemaining = entry.event.start.timeIntervalSince(date) + if criticalWindow > 0 && timeRemaining > 0 { + if timeRemaining <= criticalWindow { + if !hasShownCriticalSneakPeek { + DynamicIslandViewCoordinator.shared.toggleSneakPeek( + status: true, + type: .reminder, + duration: Defaults[.reminderSneakPeekDuration], + value: 0, + icon: ReminderLiveActivityManager.criticalIconName + ) + hasShownCriticalSneakPeek = true + } + } else { + hasShownCriticalSneakPeek = false + } } startTickerIfNeeded() } else { @@ -342,6 +368,7 @@ final class ReminderLiveActivityManager: ObservableObject { activeReminder = nil } stopTicker() + hasShownCriticalSneakPeek = false scheduleEvaluation(at: entry.triggerDate) } } @@ -355,14 +382,4 @@ final class ReminderLiveActivityManager: ObservableObject { await evaluateCurrentState(at: now) } - private func glyphName(for event: EventModel) -> String { - switch event.type { - case .birthday: - return "gift.fill" - case .reminder(let completed): - return completed ? "checkmark.circle" : "bell.fill" - case .event: - return event.isMeeting ? "person.2.fill" : "calendar" - } - } }