Merge pull request #131 from Ebullioscopic/feature/timer

Update Timer in Minimalistic UI
This commit is contained in:
Hariharan Mudaliar 2025-11-30 11:46:18 +05:30 • committed by GitHub
commit 97ede14380
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1586 additions and 553 deletions

View file

@ -203,6 +203,9 @@
knownRegions = ( knownRegions = (
en, en,
Base, Base,
hi,
ta,
gu,
); );
mainGroup = F54406DE2CD4987A00C7AFD9; mainGroup = F54406DE2CD4987A00C7AFD9;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;

View file

@ -401,6 +401,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
} }
.store(in: &cancellables) .store(in: &cancellables)
TimerManager.shared.$activeSource
.combineLatest(TimerManager.shared.$isTimerActive)
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.debouncedUpdateWindowSize()
}
.store(in: &cancellables)
Defaults.publisher(.enableShortcuts, options: []).sink { change in Defaults.publisher(.enableShortcuts, options: []).sink { change in
KeyboardShortcuts.isEnabled = change.newValue KeyboardShortcuts.isEnabled = change.newValue
}.store(in: &cancellables) }.store(in: &cancellables)

View file

@ -5228,6 +5228,10 @@
} }
} }
}, },
"Clear search" : {
"comment" : "An accessibility label for the button that clears the search field in the sidebar.",
"isCommentAutoGenerated" : true
},
"Click 'Pick Color' or use Cmd+Shift+P to start picking colors" : { "Click 'Pick Color' or use Cmd+Shift+P to start picking colors" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -5410,6 +5414,10 @@
} }
} }
}, },
"Collect the latest crash report to share with the developer when reporting lock screen or overlay issues." : {
"comment" : "A footer for the \"Diagnostics\" section of the lock screen settings.",
"isCommentAutoGenerated" : true
},
"Color Details" : { "Color Details" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -5741,6 +5749,14 @@
}, },
"Controls how often system metrics refresh while monitoring is active." : { "Controls how often system metrics refresh while monitoring is active." : {
},
"Controls the optional timer widget that floats above the media panel. Blur adds a frosted finish behind the compact view." : {
"comment" : "A footer for the \"Timer Widget\" section in the Lock Screen Settings view.",
"isCommentAutoGenerated" : true
},
"Controls whether Dynamic Island mirrors lock/unlock events with its own live activity and audible chimes." : {
"comment" : "A footer for the \"Live Activity & Feedback\" section in the Lock Screen Settings.",
"isCommentAutoGenerated" : true
}, },
"Copy event link" : { "Copy event link" : {
"comment" : "A tooltip for a button that copies the link of a reminder event to the clipboard.", "comment" : "A tooltip for a button that copies the link of a reminder event to the clipboard.",
@ -6675,6 +6691,10 @@
} }
} }
}, },
"Diagnostics" : {
"comment" : "A section header in the Lock Screen Settings view, describing diagnostics-related functionality.",
"isCommentAutoGenerated" : true
},
"Disable" : { "Disable" : {
"localizations" : { "localizations" : {
"ar" : { "ar" : {
@ -7536,6 +7556,10 @@
"comment" : "A message displayed below the section header in the IdleAnimationsSettingsSection, explaining that users can customize animations by enabling this feature.", "comment" : "A message displayed below the section header in the IdleAnimationsSettingsSection, explaining that users can customize animations by enabling this feature.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Enable and style the media controls that appear above the system clock when the screen is locked." : {
"comment" : "A description of the media panel settings in the lock screen settings view.",
"isCommentAutoGenerated" : true
},
"Enable Built-in System HUD" : { "Enable Built-in System HUD" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -7990,6 +8014,10 @@
}, },
"Enable stats monitoring in Settings to view system performance data." : { "Enable stats monitoring in Settings to view system performance data." : {
},
"Enable the weather capsule and configure its layout, provider, units, and optional battery/AQI indicators." : {
"comment" : "A footer for the \"Weather Widget\" section in the Lock Screen Settings view, describing its purpose.",
"isCommentAutoGenerated" : true
}, },
"Enable Thinking Mode" : { "Enable Thinking Mode" : {
@ -10629,6 +10657,10 @@
} }
} }
}, },
"Layout" : {
"comment" : "A label for the segment control that lets the user choose the layout of the weather widget.",
"isCommentAutoGenerated" : true
},
"License key" : { "License key" : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -10666,6 +10698,10 @@
"comment" : "A label indicating that the music is live.", "comment" : "A label indicating that the music is live.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Live Activity & Feedback" : {
"comment" : "The header for the \"Live Activity & Feedback\" section in the lock screen settings.",
"isCommentAutoGenerated" : true
},
"Live Monitoring" : { "Live Monitoring" : {
"extractionState" : "stale", "extractionState" : "stale",
"localizations" : { "localizations" : {
@ -10713,7 +10749,7 @@
} }
} }
}, },
"Lock screen material" : { "Lock Screen Integration" : {
}, },
"Lock screen panel preview" : { "Lock screen panel preview" : {
@ -11038,112 +11074,9 @@
"Mac battery at %d percent" : { "Mac battery at %d percent" : {
"comment" : "Mac battery gauge accessibility label" "comment" : "Mac battery gauge accessibility label"
}, },
"MacBook icon is available in the circular layout." : { "Made with ❤️ by Ebullioscopic" : {
"comment" : "A footer text that credits the developer of the app.",
}, "isCommentAutoGenerated" : true
"Made ❤️ by Ebullioscopic" : {
},
"Made with 🫶🏻 by not so boring not.people" : {
"extractionState" : "stale",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"cs" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Vyrobeno s 🫶🏻 ne tak nudnými ne lidmi."
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Gemacht mit 🫶🏻 durch nicht so boring not.people"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Hecho con 🫶🏻 no tan aburrido no.people"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Fait avec 🫶🏻 par des personnes pas si ennuyeuses - boring not.people"
}
},
"hu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Realizzato col 🫶🏻 da not so boring not.people"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "지루하지 않은 비사람들이 🫶🏻 마음을 담아 만들었습니다"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Zrobione z 🫶🏻 przez niezbyt nudnych ludzi"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"ru" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Made with 🫶🏻 by not so boring not.people"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "由 not.people 团队用心制作 🫶🏻"
}
}
}
}, },
"Manage indicator preferences from Settings → Privacy. No manual microphone toggle is required." : { "Manage indicator preferences from Settings → Privacy. No manual microphone toggle is required." : {
"comment" : "A description of how to manage the microphone privacy indicator preferences.", "comment" : "A description of how to manage the microphone privacy indicator preferences.",
@ -11352,6 +11285,10 @@
} }
} }
}, },
"Material" : {
"comment" : "A picker that lets the user select the material of the lock screen glass.",
"isCommentAutoGenerated" : true
},
"Max Capacity: %lld%%" : { "Max Capacity: %lld%%" : {
"localizations" : { "localizations" : {
"ar" : { "ar" : {
@ -11858,6 +11795,10 @@
"comment" : "Accessibility label describing the control that opens the media output selector.", "comment" : "Accessibility label describing the control that opens the media output selector.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Media Panel" : {
"comment" : "A section header in the lock screen settings view.",
"isCommentAutoGenerated" : true
},
"Media playback live activity" : { "Media playback live activity" : {
"localizations" : { "localizations" : {
"ar" : { "ar" : {
@ -12738,6 +12679,9 @@
} }
} }
} }
},
"Mirrors the toggle found under Lock Screen settings so timer-specific workflows can enable or disable the widget without switching tabs." : {
}, },
"Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : { "Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : {
@ -15958,6 +15902,10 @@
} }
} }
}, },
"Search Settings" : {
"comment" : "A placeholder text for a search bar in the settings view.",
"isCommentAutoGenerated" : true
},
"Search..." : { "Search..." : {
"localizations" : { "localizations" : {
"fr" : { "fr" : {
@ -17245,9 +17193,6 @@
}, },
"Show timer name" : { "Show timer name" : {
},
"Shows a lock icon in the Dynamic Island when the screen is locked. Use the toggles above to control the lock screen media panel and weather capsule." : {
}, },
"Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs." : { "Shows green camera icon and yellow microphone icon when in use. Uses event-driven CoreAudio and CoreMediaIO APIs." : {
@ -18456,9 +18401,16 @@
}, },
"Temperature unit" : { "Temperature unit" : {
},
"The MacBook icon is available only in the circular layout." : {
"comment" : "A message displayed below a toggle that lets the user choose whether to show a MacBook icon in the battery gauge of the weather widget. The message explains that the MacBook icon is only available when the weather widget is in a",
"isCommentAutoGenerated" : true
}, },
"TheDynamicIsland" : { "TheDynamicIsland" : {
},
"These controls mirror the Lock Screen tab so you can tune the media overlay while focusing on playback settings." : {
}, },
"These controls sit beside the notch while a timer runs. They require the timer name to stay hidden for spacing." : { "These controls sit beside the notch while a timer runs. They require the timer name to stay hidden for spacing." : {
@ -18637,6 +18589,10 @@
}, },
"Timer tint" : { "Timer tint" : {
},
"Timer Widget" : {
"comment" : "A section header for the timer widget settings.",
"isCommentAutoGenerated" : true
}, },
"Toggle Notch Open:" : { "Toggle Notch Open:" : {
"localizations" : { "localizations" : {
@ -19920,6 +19876,10 @@
}, },
"Weather data provider" : { "Weather data provider" : {
},
"Weather Widget" : {
"comment" : "A section header in the Lock Screen Settings view.",
"isCommentAutoGenerated" : true
}, },
"Weather: %@ %@" : { "Weather: %@ %@" : {
"comment" : "Weather description and temperature", "comment" : "Weather description and temperature",
@ -20154,9 +20114,6 @@
}, },
"When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : {
},
"Widget layout" : {
}, },
"Width:" : { "Width:" : {
"localizations" : { "localizations" : {

View file

@ -21,8 +21,11 @@ struct MinimalisticMusicPlayerView: View {
@Default(.musicAuxRightControl) private var rightAuxControl @Default(.musicAuxRightControl) private var rightAuxControl
@Default(.musicSkipBehavior) private var musicSkipBehavior @Default(.musicSkipBehavior) private var musicSkipBehavior
@ObservedObject private var reminderManager = ReminderLiveActivityManager.shared @ObservedObject private var reminderManager = ReminderLiveActivityManager.shared
@ObservedObject private var timerManager = TimerManager.shared
@ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared
@Default(.enableReminderLiveActivity) private var enableReminderLiveActivity @Default(.enableReminderLiveActivity) private var enableReminderLiveActivity
@Default(.enableLyrics) private var enableLyrics @Default(.enableLyrics) private var enableLyrics
@Default(.timerPresets) private var timerPresets
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
@ -76,6 +79,8 @@ struct MinimalisticMusicPlayerView: View {
.padding(.top, 10) .padding(.top, 10)
} }
timerCountdownSection
reminderList reminderList
} }
.padding(.horizontal, 12) .padding(.horizontal, 12)
@ -83,6 +88,7 @@ struct MinimalisticMusicPlayerView: View {
.padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding) .padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: calculateDynamicHeight(), alignment: .top) .frame(height: calculateDynamicHeight(), alignment: .top)
.animation(.smooth(duration: 0.3), value: dynamicHeightSignature)
} }
// MARK: - TypingLyricView // MARK: - TypingLyricView
@ -156,6 +162,37 @@ struct MinimalisticMusicPlayerView: View {
ReminderLiveActivityManager.additionalHeight(forRowCount: reminderEntries.count) ReminderLiveActivityManager.additionalHeight(forRowCount: reminderEntries.count)
} }
private var shouldShowTimerCountdown: Bool {
coordinator.timerLiveActivityEnabled && timerManager.isExternalTimerActive
}
private var timerCountdownColor: Color {
let baseColor: Color
if let presetId = timerManager.activePresetId,
let preset = timerPresets.first(where: { $0.id == presetId }) {
baseColor = preset.color
} else {
baseColor = timerManager.timerColor
}
return baseColor.ensureMinimumBrightness(factor: 0.75)
}
private var timerCountdownText: String {
timerManager.formattedRemainingTime()
}
private var timerDisplayName: String {
let trimmed = timerManager.timerName.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "Timer" : trimmed
}
private var dynamicHeightSignature: Int {
var signature = reminderEntries.count * 10
if enableLyrics { signature += 1 }
if shouldShowTimerCountdown { signature += 100 }
return signature
}
private func calculateDynamicHeight() -> CGFloat { private func calculateDynamicHeight() -> CGFloat {
var height: CGFloat = 50 // Base height for header var height: CGFloat = 50 // Base height for header
@ -172,6 +209,10 @@ struct MinimalisticMusicPlayerView: View {
height += lyricsTopPadding + lyricsEstimatedHeight height += lyricsTopPadding + lyricsEstimatedHeight
} }
if shouldShowTimerCountdown {
height += minimalisticTimerCountdownBlockHeight
}
// Add reminder list height if showing // Add reminder list height if showing
if shouldShowReminderList { if shouldShowReminderList {
height += reminderListHeight height += reminderListHeight
@ -184,6 +225,60 @@ struct MinimalisticMusicPlayerView: View {
return height return height
} }
private var timerCountdownSection: some View {
VStack(spacing: 0) {
if shouldShowTimerCountdown {
timerCountdownView
.transition(.opacity.combined(with: .move(edge: .top)))
}
}
.padding(.top, shouldShowTimerCountdown ? minimalisticTimerCountdownTopPadding : 0)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: shouldShowTimerCountdown ? minimalisticTimerCountdownBlockHeight : 0, alignment: .top)
.animation(.smooth(duration: 0.25), value: shouldShowTimerCountdown)
}
private func displayFont(size: CGFloat) -> Font {
.custom("SF Pro Display", size: size)
}
private var timerCountdownView: some View {
GeometryReader { geometry in
let availableWidth = geometry.size.width
let preferredCountdownWidth = max(availableWidth * 0.42, 150)
let maxCountdownWidth = max(availableWidth - 60, 0)
let countdownWidth = min(preferredCountdownWidth, maxCountdownWidth)
let marqueeWidth = max(availableWidth - countdownWidth - 12, 0)
HStack(alignment: .lastTextBaseline, spacing: 12) {
MarqueeText(
.init(get: { timerDisplayName }, set: { _ in }),
font: .system(size: 16, weight: .semibold),
nsFont: .title3,
textColor: timerCountdownColor.opacity(0.85),
frameWidth: max(marqueeWidth, 1)
)
.alignmentGuide(.lastTextBaseline) { dimensions in
dimensions[VerticalAlignment.bottom]
}
Spacer(minLength: 8)
Text(timerCountdownText)
.font(displayFont(size: 56))
.monospacedDigit()
.foregroundStyle(timerManager.isOvertime ? Color.red : timerCountdownColor)
.contentTransition(.numericText())
.animation(.smooth(duration: 0.25), value: timerManager.remainingTime)
.lineLimit(1)
.frame(width: countdownWidth, alignment: .trailing)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 4)
.frame(height: minimalisticTimerCountdownContentHeight, alignment: .top)
}
private var reminderList: some View { private var reminderList: some View {
MinimalisticReminderEventListView(reminders: reminderEntries) MinimalisticReminderEventListView(reminders: reminderEntries)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)

File diff suppressed because it is too large Load diff

View file

@ -37,6 +37,7 @@ struct TimerLiveActivity: View {
@Default(.timerSolidColor) private var solidColor @Default(.timerSolidColor) private var solidColor
@Default(.timerPresets) private var timerPresets @Default(.timerPresets) private var timerPresets
@Default(.timerControlWindowEnabled) private var controlWindowEnabled @Default(.timerControlWindowEnabled) private var controlWindowEnabled
@Default(.enableMinimalisticUI) private var enableMinimalisticUI
private var notchContentHeight: CGFloat { private var notchContentHeight: CGFloat {
max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
@ -614,6 +615,7 @@ struct TimerLiveActivity: View {
private func triggerTransientLabel() { private func triggerTransientLabel() {
guard !showsLabel else { return } guard !showsLabel else { return }
guard !enableMinimalisticUI else { return }
cancelTransientLabel() cancelTransientLabel()
withAnimation(.smooth) { withAnimation(.smooth) {
showTransientLabel = true showTransientLabel = true

View file

@ -119,6 +119,14 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
} }
.store(in: &cancellables) .store(in: &cancellables)
TimerManager.shared.$activeSource
.combineLatest(TimerManager.shared.$isTimerActive)
.receive(on: RunLoop.main)
.sink { [weak self] _, _ in
self?.handleMinimalisticTimerHeightChange()
}
.store(in: &cancellables)
coordinator.$statsSecondRowExpansion coordinator.$statsSecondRowExpansion
.removeDuplicates() .removeDuplicates()
.receive(on: RunLoop.main) .receive(on: RunLoop.main)
@ -141,6 +149,23 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
.store(in: &cancellables) .store(in: &cancellables)
} }
private func handleMinimalisticTimerHeightChange() {
guard Defaults[.enableMinimalisticUI] else { return }
guard notchState == .open else { return }
let updatedTarget = calculateDynamicNotchSize()
guard notchSize != updatedTarget else { return }
withAnimation(.smooth) {
notchSize = updatedTarget
}
if let delegate = AppDelegate.shared {
delegate.ensureWindowSize(
addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]),
animated: true,
force: false
)
}
}
private func setupDetectorObserver() { private func setupDetectorObserver() {
// 1) Publisher for the user’s fullscreen detection setting // 1) Publisher for the user’s fullscreen detection setting
let enabledPublisher = Defaults let enabledPublisher = Defaults

View file

@ -17,6 +17,9 @@ let batterySneakSize: CGSize = .init(width: 160, height: 1)
let openNotchSize: CGSize = .init(width: 640, height: 190) let openNotchSize: CGSize = .init(width: 640, height: 190)
private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 180) private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 180)
private let minimalisticLyricsExtraHeight: CGFloat = 40 private let minimalisticLyricsExtraHeight: CGFloat = 40
let minimalisticTimerCountdownTopPadding: CGFloat = 12
let minimalisticTimerCountdownContentHeight: CGFloat = 82
let minimalisticTimerCountdownBlockHeight: CGFloat = minimalisticTimerCountdownTopPadding + minimalisticTimerCountdownContentHeight
let statsSecondRowContentHeight: CGFloat = 120 let statsSecondRowContentHeight: CGFloat = 120
let statsGridSpacingHeight: CGFloat = 12 let statsGridSpacingHeight: CGFloat = 12
let notchShadowPaddingStandard: CGFloat = 18 let notchShadowPaddingStandard: CGFloat = 18
@ -36,6 +39,10 @@ var minimalisticOpenNotchSize: CGSize {
size.height += reminderHeight size.height += reminderHeight
} }
if DynamicIslandViewCoordinator.shared.timerLiveActivityEnabled && TimerManager.shared.isExternalTimerActive {
size.height += minimalisticTimerCountdownBlockHeight
}
return size return size
} }
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14)) let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))