Working Timer in Minimalistic View

This commit is contained in:
Hariharan Mudaliar 2025-11-30 01:41:45 +05:30
commit 8a76d9e86f
4 changed files with 135 additions and 0 deletions

View file

@ -401,6 +401,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
.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
KeyboardShortcuts.isEnabled = change.newValue
}.store(in: &cancellables)

View file

@ -21,8 +21,11 @@ struct MinimalisticMusicPlayerView: View {
@Default(.musicAuxRightControl) private var rightAuxControl
@Default(.musicSkipBehavior) private var musicSkipBehavior
@ObservedObject private var reminderManager = ReminderLiveActivityManager.shared
@ObservedObject private var timerManager = TimerManager.shared
@ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared
@Default(.enableReminderLiveActivity) private var enableReminderLiveActivity
@Default(.enableLyrics) private var enableLyrics
@Default(.timerPresets) private var timerPresets
var body: some View {
VStack(spacing: 0) {
@ -76,6 +79,8 @@ struct MinimalisticMusicPlayerView: View {
.padding(.top, 10)
}
timerCountdownSection
reminderList
}
.padding(.horizontal, 12)
@ -83,6 +88,7 @@ struct MinimalisticMusicPlayerView: View {
.padding(.bottom, ReminderLiveActivityManager.baselineMinimalisticBottomPadding)
.frame(maxWidth: .infinity)
.frame(height: calculateDynamicHeight(), alignment: .top)
.animation(.smooth(duration: 0.3), value: dynamicHeightSignature)
}
// MARK: - TypingLyricView
@ -156,6 +162,37 @@ struct MinimalisticMusicPlayerView: View {
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 {
var height: CGFloat = 50 // Base height for header
@ -172,6 +209,10 @@ struct MinimalisticMusicPlayerView: View {
height += lyricsTopPadding + lyricsEstimatedHeight
}
if shouldShowTimerCountdown {
height += minimalisticTimerCountdownBlockHeight
}
// Add reminder list height if showing
if shouldShowReminderList {
height += reminderListHeight
@ -184,6 +225,60 @@ struct MinimalisticMusicPlayerView: View {
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 {
MinimalisticReminderEventListView(reminders: reminderEntries)
.frame(maxWidth: .infinity, alignment: .leading)

View file

@ -119,6 +119,14 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
}
.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
.removeDuplicates()
.receive(on: RunLoop.main)
@ -141,6 +149,23 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
.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() {
// 1) Publisher for the user’s fullscreen detection setting
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)
private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 180)
private let minimalisticLyricsExtraHeight: CGFloat = 40
let minimalisticTimerCountdownTopPadding: CGFloat = 12
let minimalisticTimerCountdownContentHeight: CGFloat = 82
let minimalisticTimerCountdownBlockHeight: CGFloat = minimalisticTimerCountdownTopPadding + minimalisticTimerCountdownContentHeight
let statsSecondRowContentHeight: CGFloat = 120
let statsGridSpacingHeight: CGFloat = 12
let notchShadowPaddingStandard: CGFloat = 18
@ -36,6 +39,10 @@ var minimalisticOpenNotchSize: CGSize {
size.height += reminderHeight
}
if DynamicIslandViewCoordinator.shared.timerLiveActivityEnabled && TimerManager.shared.isExternalTimerActive {
size.height += minimalisticTimerCountdownBlockHeight
}
return size
}
let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14))