diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index f9a7179..ed699f5 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -406,7 +406,7 @@ struct ContentView: View { .frame(width: 76, alignment: .trailing) } .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) - } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { + } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress) .transition(.opacity) } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .music) && vm.notchState == .closed && (musicManager.isPlaying || !musicManager.isPlayerIdle) && coordinator.musicLiveActivityEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked { @@ -469,6 +469,19 @@ struct ContentView: View { .padding(.bottom, 10) } } + else if coordinator.sneakPeek.type == .reminder { + if !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard, let reminder = reminderManager.activeReminder { + GeometryReader { geo in + MarqueeText( + .constant(reminderSneakPeekText(for: reminder)), + textColor: reminderColor(for: reminder.event), + minDuration: 1, + frameWidth: geo.size.width + ) + } + .padding(.bottom, 10) + } + } } } } @@ -509,6 +522,32 @@ struct ContentView: View { } } + private func reminderColor(for event: EventModel) -> Color { + Color(nsColor: event.calendar.color).ensureMinimumBrightness(factor: 0.7) + } + + private func reminderSneakPeekText(for entry: ReminderLiveActivityManager.ReminderEntry) -> String { + let title = entry.event.title.isEmpty ? "Upcoming Reminder" : entry.event.title + let remaining = max(entry.event.start.timeIntervalSince(reminderManager.currentDate), 0) + let minutes = Int(ceil(remaining / 60)) + let timeString = reminderTimeFormatter.string(from: entry.event.start) + + if minutes <= 0 { + return "\(title) • starts now @ \(timeString)" + } else if minutes == 1 { + return "\(title) • in 1 min @ \(timeString)" + } else { + return "\(title) • in \(minutes) min @ \(timeString)" + } + } + + private let reminderTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + @ViewBuilder func DynamicIslandFaceAnimation() -> some View { HStack { diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index 0d4b7b8..febe004 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -144,9 +144,17 @@ class DynamicIslandViewCoordinator: ObservableObject { } func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") { - let resolvedDuration = type == .timer ? 10 : duration + let resolvedDuration: TimeInterval + switch type { + case .timer: + resolvedDuration = 10 + case .reminder: + resolvedDuration = Defaults[.reminderSneakPeekDuration] + default: + resolvedDuration = duration + } sneakPeekDuration = resolvedDuration - if type != .music && type != .timer { + if type != .music && type != .timer && type != .reminder { // close() if !hudReplacement { return diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 7567aca..0e27a7b 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -872,6 +872,10 @@ "comment" : "A text label displaying the current value of the \"Notify before\" slider. The argument is the current value of the slider, expressed in minutes.", "isCommentAutoGenerated" : true }, + "%lld s" : { + "comment" : "A text view displaying the duration of the \"sneak peek\" feature in the reminder live activity section of the calendar settings view. The text is followed by a unit of measurement (\"s\").", + "isCommentAutoGenerated" : true + }, "%lld%%" : { "localizations" : { "ar" : { @@ -17177,6 +17181,10 @@ "comment" : "A description of how the color-coding feature works.", "isCommentAutoGenerated" : true }, + "Sneak peek duration" : { + "comment" : "A label describing the duration of the \"sneak peek\" feature in the calendar settings.", + "isCommentAutoGenerated" : true + }, "Sneak Peek shows the media title and artist under the notch for a few seconds." : { "localizations" : { "ar" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index c8e0346..807c757 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -956,6 +956,7 @@ struct CalendarSettings: View { @Default(.enableReminderLiveActivity) var enableReminderLiveActivity @Default(.reminderPresentationStyle) var reminderPresentationStyle @Default(.reminderLeadTime) var reminderLeadTime + @Default(.reminderSneakPeekDuration) var reminderSneakPeekDuration var body: some View { Form { @@ -1026,6 +1027,19 @@ struct CalendarSettings: View { .foregroundStyle(.secondary) .frame(width: 60, alignment: .trailing) } + + HStack { + Text("Sneak peek duration") + Slider( + value: $reminderSneakPeekDuration, + in: 3...20, + step: 1 + ) + .disabled(!enableReminderLiveActivity) + Text("\(Int(reminderSneakPeekDuration)) s") + .foregroundStyle(.secondary) + .frame(width: 60, alignment: .trailing) + } } Section(header: Text("Select Calendars")) { diff --git a/DynamicIsland/managers/ReminderLiveActivityManager.swift b/DynamicIsland/managers/ReminderLiveActivityManager.swift index 07ed6a1..05966f9 100644 --- a/DynamicIsland/managers/ReminderLiveActivityManager.swift +++ b/DynamicIsland/managers/ReminderLiveActivityManager.swift @@ -259,6 +259,13 @@ final class ReminderLiveActivityManager: ObservableObject { } if activeReminder != entry { activeReminder = entry + DynamicIslandViewCoordinator.shared.toggleSneakPeek( + status: true, + type: .reminder, + duration: Defaults[.reminderSneakPeekDuration], + value: 0, + icon: glyphName(for: entry.event) + ) } startTickerIfNeeded() } else { @@ -269,4 +276,15 @@ final class ReminderLiveActivityManager: ObservableObject { scheduleEvaluation(at: entry.triggerDate) } } + + 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" + } + } } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 4ebbe16..c4e4692 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -463,6 +463,7 @@ extension Defaults.Keys { static let enableReminderLiveActivity = Key("enableReminderLiveActivity", default: true) static let reminderPresentationStyle = Key("reminderPresentationStyle", default: .ringCountdown) static let reminderLeadTime = Key("reminderLeadTime", default: 5) + static let reminderSneakPeekDuration = Key("reminderSneakPeekDuration", default: 10) static let timerControlWindowEnabled = Key("timerControlWindowEnabled", default: true) // MARK: ColorPicker Feature