BugFix stuff

This commit is contained in:
Hariharan Mudaliar 2026-01-12 00:24:58 +05:30
commit 911ed0da21
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E

View file

@ -12,6 +12,11 @@ import Defaults
import KeyboardShortcuts import KeyboardShortcuts
import SwiftUI import SwiftUI
import SwiftUIIntrospect import SwiftUIIntrospect
#if canImport(AppKit)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif
@MainActor @MainActor
struct ContentView: View { struct ContentView: View {
@ -520,17 +525,15 @@ struct ContentView: View {
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2))) .transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && (doNotDisturbManager.isDoNotDisturbActive || Defaults[.focusIndicatorNonPersistent]) && !vm.hideOnClosed && !lockScreenManager.isLocked { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .doNotDisturb) && vm.notchState == .closed && Defaults[.enableDoNotDisturbDetection] && Defaults[.showDoNotDisturbIndicator] && (doNotDisturbManager.isDoNotDisturbActive || Defaults[.focusIndicatorNonPersistent]) && !vm.hideOnClosed && !lockScreenManager.isLocked {
DoNotDisturbLiveActivity() DoNotDisturbLiveActivity()
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .lockScreen) && vm.notchState == .closed && (lockScreenManager.isLocked || !lockScreenManager.isLockIdle) && Defaults[.enableLockScreenLiveActivity] && !vm.hideOnClosed {
LockScreenLiveActivity() LockScreenLiveActivity()
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed {
PrivacyLiveActivity() PrivacyLiveActivity()
} else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed { } else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle) DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle)
} else if vm.notchState == .open { } else if vm.notchState == .open {
DynamicIslandHeader() DynamicIslandHeader()
.frame(height: max(24, vm.effectiveClosedNotchHeight)) .frame(height: max(24, vm.effectiveClosedNotchHeight))
.blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0)
.animation(.spring(response: 1, dampingFraction: 1, blendDuration: 0.8), value: vm.notchState)
} else { } else {
Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight) Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight)
} }
@ -688,12 +691,23 @@ struct ContentView: View {
let secondary = resolveMusicSecondaryLiveActivity() let secondary = resolveMusicSecondaryLiveActivity()
let notchContentHeight = max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) let notchContentHeight = max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
let wingBaseWidth = max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2) let wingBaseWidth = max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2)
let centerBaseWidth = vm.closedNotchSize.width + (isHovering ? 8 : 0) let rawCenterBaseWidth = vm.closedNotchSize.width + (isHovering ? 8 : 0)
let inlineSneakPeekActive = (coordinator.expandingView.show && (coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer) && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) let centerBaseWidth = max(rawCenterBaseWidth, 96)
let rightWingWidth = resolvedRightWingWidth(for: secondary, baseWidth: wingBaseWidth, centerBaseWidth: centerBaseWidth) let inlineSneakPeekActive = (
let centerWidth = resolvedCenterWidth(baseWidth: centerBaseWidth, baseRightWidth: wingBaseWidth, desiredRightWidth: rightWingWidth) coordinator.expandingView.show &&
(coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer) &&
Defaults[.enableSneakPeek] &&
Defaults[.sneakPeekStyles] == .inline
)
let rightWingWidth = resolvedRightWingWidth(
for: secondary,
baseWidth: wingBaseWidth,
centerBaseWidth: centerBaseWidth,
notchHeight: notchContentHeight
)
let effectiveCenterWidth = inlineSneakPeekActive ? 380 : centerBaseWidth
let notchWidth = wingBaseWidth + effectiveCenterWidth + rightWingWidth
let badgeSize = max(16, notchContentHeight * 0.45) let badgeSize = max(16, notchContentHeight * 0.45)
let effectiveCenterWidth = inlineSneakPeekActive ? 380 : centerWidth
HStack(spacing: 0) { HStack(spacing: 0) {
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
@ -729,7 +743,6 @@ struct ContentView: View {
) )
.opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0) .opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
Spacer(minLength: vm.closedNotchSize.width) Spacer(minLength: vm.closedNotchSize.width)
// Song Artist
Text(musicManager.artistName) Text(musicManager.artistName)
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
@ -744,7 +757,6 @@ struct ContentView: View {
) )
.opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0) .opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
Spacer(minLength: vm.closedNotchSize.width) Spacer(minLength: vm.closedNotchSize.width)
// Timer Status
Text(timerManager.formattedRemainingTime()) Text(timerManager.formattedRemainingTime())
.lineLimit(1) .lineLimit(1)
.truncationMode(.tail) .truncationMode(.tail)
@ -754,11 +766,12 @@ struct ContentView: View {
} }
) )
musicRightWing(for: secondary) musicRightWing(for: secondary, notchHeight: notchContentHeight)
.frame(width: rightWingWidth, height: notchContentHeight, alignment: .center) .frame(width: rightWingWidth, height: notchContentHeight, alignment: .center)
.id(secondary?.id ?? "music-spectrum") .id(secondary?.id ?? "music-spectrum")
.contentTransition(.symbolEffect(.replace)) .contentTransition(.symbolEffect(.replace))
} }
.frame(width: notchWidth, height: notchContentHeight)
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
.animation(.smooth(duration: 0.25), value: secondary?.id) .animation(.smooth(duration: 0.25), value: secondary?.id)
} }
@ -792,32 +805,81 @@ struct ContentView: View {
return nil return nil
} }
private func resolvedRightWingWidth(for secondary: MusicSecondaryLiveActivity?, baseWidth: CGFloat, centerBaseWidth: CGFloat) -> CGFloat { private func resolvedRightWingWidth(for secondary: MusicSecondaryLiveActivity?, baseWidth: CGFloat, centerBaseWidth: CGFloat, notchHeight: CGFloat) -> CGFloat {
guard let secondary else { return baseWidth } guard let secondary else { return baseWidth }
func scaledWidth(factor: CGFloat, extra: CGFloat = 0) -> CGFloat {
max(baseWidth, max(centerBaseWidth * factor, baseWidth + extra))
}
switch secondary { switch secondary {
case .timer: case .timer:
return scaledWidth(factor: 0.58, extra: 28) return timerRightWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth)
case .reminder: case .reminder(let entry):
return scaledWidth(factor: 0.52, extra: 22) return reminderRightWingWidth(for: entry, baseWidth: baseWidth, notchHeight: notchHeight, now: reminderManager.currentDate)
case .capsLock(let showLabel): case .capsLock(let showLabel):
return showLabel ? scaledWidth(factor: 0.4, extra: 12) : baseWidth return showLabel ? scaledWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth, factor: 0.4, extra: 12) : baseWidth
case .coolFace: case .coolFace:
return scaledWidth(factor: 0.38, extra: 8) return scaledWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth, factor: 0.36, extra: 8)
case .focus: case .focus:
return scaledWidth(factor: 0.42, extra: 10) return focusRightWingWidth(baseWidth: baseWidth)
case .recording: case .recording:
return scaledWidth(factor: 0.45, extra: 14) return recordingRightWingWidth(baseWidth: baseWidth)
} }
} }
private func resolvedCenterWidth(baseWidth: CGFloat, baseRightWidth: CGFloat, desiredRightWidth: CGFloat) -> CGFloat { private func timerRightWingWidth(baseWidth: CGFloat, centerBaseWidth: CGFloat) -> CGFloat {
let delta = max(0, desiredRightWidth - baseRightWidth) if timerShowsCountdown {
return max(96, baseWidth - delta) return timerCountdownWingWidth(baseWidth: baseWidth)
}
let showsProgress = timerShowsProgress
let usesRingProgress = timerProgressStyle == .ring
switch (showsProgress, usesRingProgress) {
case (true, true):
return scaledWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth, factor: 0.46, extra: 18)
case (true, false):
return scaledWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth, factor: 0.52, extra: 24)
case (false, _):
return scaledWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth, factor: 0.38, extra: 12)
}
}
private func timerCountdownWingWidth(baseWidth: CGFloat) -> CGFloat {
let padding: CGFloat = 18
let ringWidth: CGFloat = (timerShowsProgress && timerProgressStyle == .ring) ? 30 : 0
let spacing: CGFloat = (ringWidth > 0) ? 8 : 0
let countdownText = timerManager.formattedRemainingTime()
let countdownWidth = TimerSupplementMetrics.countdownFrameWidth(for: countdownText)
return max(baseWidth, padding + ringWidth + spacing + countdownWidth)
}
private func reminderRightWingWidth(for entry: ReminderLiveActivityManager.ReminderEntry, baseWidth: CGFloat, notchHeight: CGFloat, now: Date) -> CGFloat {
let padding: CGFloat = 16
switch reminderPresentationStyle {
case .ringCountdown:
let diameter = ReminderSupplementMetrics.ringDiameter(for: notchHeight)
return max(baseWidth, padding + diameter)
case .digital:
let countdownText = ReminderSupplementMetrics.digitalCountdownText(for: entry, now: now)
let width = ReminderSupplementMetrics.digitalFrameWidth(for: countdownText)
return max(baseWidth, padding + width)
case .minutes:
let minutesText = ReminderSupplementMetrics.minutesCountdownText(for: entry, now: now)
let width = ReminderSupplementMetrics.minutesFrameWidth(for: minutesText)
return max(baseWidth, padding + width)
}
}
private func focusRightWingWidth(baseWidth: CGFloat) -> CGFloat {
// Focus pairings now mirror the default music spectrum width to keep the notch compact.
return baseWidth
}
private func recordingRightWingWidth(baseWidth: CGFloat) -> CGFloat {
// Recording keeps the spectrum but should stay near the default width with a small inset.
return max(baseWidth + 18, 54)
}
private func scaledWingWidth(baseWidth: CGFloat, centerBaseWidth: CGFloat, factor: CGFloat, extra: CGFloat) -> CGFloat {
max(baseWidth, max(centerBaseWidth * factor, baseWidth + extra))
} }
@ViewBuilder @ViewBuilder
@ -834,7 +896,7 @@ struct ContentView: View {
.foregroundStyle(timerAccentColor) .foregroundStyle(timerAccentColor)
case .reminder(let entry): case .reminder(let entry):
let accent = reminderColor(for: entry, now: reminderManager.currentDate) let accent = reminderColor(for: entry, now: reminderManager.currentDate)
Image(systemName: "calendar.badge.clock") Image(systemName: "clock")
.font(.system(size: badgeSize * 0.55, weight: .semibold)) .font(.system(size: badgeSize * 0.55, weight: .semibold))
.foregroundStyle(accent) .foregroundStyle(accent)
case .focus(let mode): case .focus(let mode):
@ -866,7 +928,7 @@ struct ContentView: View {
} }
@ViewBuilder @ViewBuilder
private func musicRightWing(for secondary: MusicSecondaryLiveActivity?) -> some View { private func musicRightWing(for secondary: MusicSecondaryLiveActivity?, notchHeight: CGFloat) -> some View {
switch secondary { switch secondary {
case .timer: case .timer:
MusicTimerSupplementView( MusicTimerSupplementView(
@ -874,14 +936,16 @@ struct ContentView: View {
accentColor: timerAccentColor, accentColor: timerAccentColor,
showsCountdown: timerShowsCountdown, showsCountdown: timerShowsCountdown,
showsProgress: timerShowsProgress, showsProgress: timerShowsProgress,
progressStyle: timerProgressStyle progressStyle: timerProgressStyle,
notchHeight: notchHeight
) )
case .reminder(let entry): case .reminder(let entry):
MusicReminderSupplementView( MusicReminderSupplementView(
entry: entry, entry: entry,
now: reminderManager.currentDate, now: reminderManager.currentDate,
style: reminderPresentationStyle, style: reminderPresentationStyle,
accent: reminderColor(for: entry, now: reminderManager.currentDate) accent: reminderColor(for: entry, now: reminderManager.currentDate),
notchHeight: notchHeight
) )
case .capsLock(let showLabel): case .capsLock(let showLabel):
if showLabel { if showLabel {
@ -893,15 +957,17 @@ struct ContentView: View {
IdleAnimationView() IdleAnimationView()
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.horizontal, 4) .padding(.horizontal, 4)
case .focus, .recording: case .focus(let mode):
spectrumView(forceSpectrum: true) spectrumView(forceSpectrum: true)
case .recording:
spectrumView(forceSpectrum: true, trailingInset: 6)
case .none: case .none:
spectrumView(forceSpectrum: false) spectrumView(forceSpectrum: false)
} }
} }
@ViewBuilder @ViewBuilder
private func spectrumView(forceSpectrum: Bool) -> some View { private func spectrumView(forceSpectrum: Bool, trailingInset: CGFloat = 0) -> some View {
if useMusicVisualizer || forceSpectrum { if useMusicVisualizer || forceSpectrum {
Rectangle() Rectangle()
.fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient) .fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient)
@ -912,6 +978,7 @@ struct ContentView: View {
.frame(width: 16, height: 12) .frame(width: 16, height: 12)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(.trailing, trailingInset)
} else { } else {
LottieAnimationView() LottieAnimationView()
.frame(maxWidth: .infinity, maxHeight: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity)
@ -1462,65 +1529,113 @@ private struct MusicTimerSupplementView: View {
let showsCountdown: Bool let showsCountdown: Bool
let showsProgress: Bool let showsProgress: Bool
let progressStyle: TimerProgressStyle let progressStyle: TimerProgressStyle
let notchHeight: CGFloat
private var clampedProgress: Double { private var clampedProgress: Double {
min(max(timerManager.progress, 0), 1) min(max(timerManager.progress, 0), 1)
} }
private var showsRingProgress: Bool {
showsProgress && progressStyle == .ring
}
private var showsBarProgress: Bool {
showsProgress && progressStyle == .bar
}
private var countdownText: String {
timerManager.formattedRemainingTime()
}
private var countdownTextWidth: CGFloat {
max(1, TimerSupplementMetrics.countdownTextWidth(for: countdownText))
}
private var countdownFrameWidth: CGFloat {
TimerSupplementMetrics.countdownFrameWidth(for: countdownText)
}
private var timerNameFrameWidth: CGFloat {
TimerSupplementMetrics.timerNameFrameWidth(for: timerManager.timerName)
}
private var ringDiameter: CGFloat {
max(min(notchHeight - 4, 26), 20)
}
var body: some View { var body: some View {
HStack(spacing: 10) { HStack(spacing: showsRingProgress && showsCountdown ? 8 : 0) {
if showsProgress { if showsRingProgress {
if progressStyle == .ring { ringView
ZStack {
Circle()
.stroke(Color.white.opacity(0.18), lineWidth: 3)
Circle()
.trim(from: 0, to: clampedProgress)
.stroke(accentColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.smooth(duration: 0.25), value: clampedProgress)
}
.frame(width: 24, height: 24)
} else if !showsCountdown {
barView()
}
} }
if showsCountdown { if showsCountdown {
VStack(alignment: .trailing, spacing: 2) { countdownStack
Text(timerManager.formattedRemainingTime()) } else if showsBarProgress {
.font(.system(size: 13, weight: .semibold, design: .monospaced)) standaloneBarView
.foregroundColor(timerManager.isOvertime ? .red : .white) } else {
.contentTransition(.numericText()) timerNameView
.animation(.smooth(duration: 0.25), value: timerManager.remainingTime)
if showsProgress && progressStyle == .bar {
barView()
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
} else if !showsProgress {
Text(timerManager.timerName)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.white)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .trailing)
} }
} }
.frame(maxWidth: .infinity, alignment: .trailing) .frame(maxWidth: .infinity, alignment: .trailing)
} }
private func barView() -> some View { private var countdownStack: some View {
VStack(alignment: .trailing, spacing: showsBarProgress ? 4 : 0) {
Text(countdownText)
.font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(timerManager.isOvertime ? .red : .white)
.contentTransition(.numericText())
.animation(.smooth(duration: 0.25), value: timerManager.remainingTime)
.frame(width: countdownFrameWidth, alignment: .trailing)
if showsBarProgress {
barView(width: countdownTextWidth)
}
}
.padding(.trailing, 8)
.frame(maxWidth: .infinity, alignment: .trailing)
}
private var ringView: some View {
ZStack {
Circle()
.stroke(Color.white.opacity(0.18), lineWidth: 3)
Circle()
.trim(from: 0, to: clampedProgress)
.stroke(accentColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.smooth(duration: 0.25), value: clampedProgress)
}
.frame(width: ringDiameter, height: ringDiameter)
.frame(width: max(ringDiameter + 4, 30), height: notchHeight, alignment: .center)
}
private var standaloneBarView: some View {
barView(width: 68)
.frame(maxWidth: .infinity, alignment: .trailing)
}
private var timerNameView: some View {
Text(timerManager.timerName)
.font(.system(size: 12, weight: .medium))
.foregroundColor(.white)
.lineLimit(1)
.frame(width: timerNameFrameWidth, alignment: .trailing)
}
private func barView(width: CGFloat) -> some View {
Capsule() Capsule()
.fill(Color.white.opacity(0.15)) .fill(Color.white.opacity(0.15))
.frame(width: 68, height: 4) .frame(width: width, height: 4)
.overlay(alignment: .leading) { .overlay(alignment: .leading) {
Capsule() Capsule()
.fill(accentColor) .fill(accentColor)
.frame(width: 68 * max(0, CGFloat(clampedProgress)), height: 4) .frame(width: width * max(0, CGFloat(clampedProgress)), height: 4)
.animation(.smooth(duration: 0.25), value: clampedProgress) .animation(.smooth(duration: 0.25), value: clampedProgress)
} }
} }
} }
private struct MusicReminderSupplementView: View { private struct MusicReminderSupplementView: View {
@ -1528,6 +1643,53 @@ private struct MusicReminderSupplementView: View {
let now: Date let now: Date
let style: ReminderPresentationStyle let style: ReminderPresentationStyle
let accent: Color let accent: Color
let notchHeight: CGFloat
var body: some View {
Group {
switch style {
case .ringCountdown:
ringCountdownView
case .digital:
digitalCountdownView
case .minutes:
minutesCountdownView
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
}
private var ringCountdownView: some View {
ZStack {
Circle()
.stroke(Color.white.opacity(0.15), lineWidth: 3)
Circle()
.trim(from: 0, to: progressValue)
.stroke(accent, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.smooth(duration: 0.25), value: progressValue)
}
.frame(width: ringDiameter, height: ringDiameter)
.frame(width: max(ringDiameter + 4, 26), height: notchHeight, alignment: .center)
}
private var digitalCountdownView: some View {
Text(digitalCountdownText)
.font(.system(size: 15, weight: .semibold, design: .monospaced))
.foregroundColor(accent)
.contentTransition(.numericText())
.animation(.smooth(duration: 0.25), value: digitalCountdownText)
.frame(width: digitalFrameWidth, alignment: .trailing)
.frame(height: notchHeight, alignment: .center)
}
private var minutesCountdownView: some View {
Text(minutesCountdownText)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(accent)
.frame(width: minutesFrameWidth, alignment: .trailing)
.frame(height: notchHeight, alignment: .center)
}
private var progressValue: Double { private var progressValue: Double {
guard entry.leadTime > 0 else { return 1 } guard entry.leadTime > 0 else { return 1 }
@ -1537,48 +1699,23 @@ private struct MusicReminderSupplementView: View {
} }
private var digitalCountdownText: String { private var digitalCountdownText: String {
let remaining = max(entry.event.start.timeIntervalSince(now), 0) ReminderSupplementMetrics.digitalCountdownText(for: entry, now: now)
let totalSeconds = Int(remaining.rounded(.down))
let minutes = totalSeconds / 60
let seconds = totalSeconds % 60
return String(format: "%02d:%02d", minutes, seconds)
} }
private var minutesCountdownText: String { private var minutesCountdownText: String {
let remaining = max(entry.event.start.timeIntervalSince(now), 0) ReminderSupplementMetrics.minutesCountdownText(for: entry, now: now)
let minutes = max(1, Int(ceil(remaining / 60)))
return minutes == 1 ? "in 1 min" : "in \(minutes) min"
} }
var body: some View { private var ringDiameter: CGFloat {
switch style { ReminderSupplementMetrics.ringDiameter(for: notchHeight)
case .ringCountdown: }
ZStack {
Circle()
.stroke(Color.white.opacity(0.15), lineWidth: 3)
Circle()
.trim(from: 0, to: progressValue)
.stroke(accent, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.smooth(duration: 0.25), value: progressValue)
}
.frame(width: 24, height: 24)
.frame(maxWidth: .infinity, alignment: .trailing)
case .digital: private var digitalFrameWidth: CGFloat {
Text(digitalCountdownText) ReminderSupplementMetrics.digitalFrameWidth(for: digitalCountdownText)
.font(.system(size: 15, weight: .semibold, design: .monospaced)) }
.foregroundColor(accent)
.contentTransition(.numericText())
.animation(.smooth(duration: 0.25), value: digitalCountdownText)
.frame(maxWidth: .infinity, alignment: .trailing)
case .minutes: private var minutesFrameWidth: CGFloat {
Text(minutesCountdownText) ReminderSupplementMetrics.minutesFrameWidth(for: minutesCountdownText)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(accent)
.frame(maxWidth: .infinity, alignment: .trailing)
}
} }
} }
@ -1595,6 +1732,64 @@ private struct MusicCapsLockLabelView: View {
} }
} }
#if canImport(AppKit)
private typealias MusicSupplementFont = NSFont
#elseif canImport(UIKit)
private typealias MusicSupplementFont = UIFont
#endif
private enum TimerSupplementMetrics {
static func countdownTextWidth(for text: String) -> CGFloat {
musicMeasureText(text, font: MusicSupplementFont.monospacedDigitSystemFont(ofSize: 13, weight: .semibold))
}
static func countdownFrameWidth(for text: String) -> CGFloat {
max(countdownTextWidth(for: text) + 16, 72)
}
static func timerNameFrameWidth(for text: String) -> CGFloat {
guard !text.isEmpty else { return 64 }
let width = musicMeasureText(text, font: MusicSupplementFont.systemFont(ofSize: 12, weight: .medium))
return max(width + 14, 64)
}
}
private enum ReminderSupplementMetrics {
static func digitalCountdownText(for entry: ReminderLiveActivityManager.ReminderEntry, now: Date) -> String {
let remaining = max(entry.event.start.timeIntervalSince(now), 0)
let totalSeconds = Int(remaining.rounded(.down))
let minutes = totalSeconds / 60
let seconds = totalSeconds % 60
return String(format: "%02d:%02d", minutes, seconds)
}
static func minutesCountdownText(for entry: ReminderLiveActivityManager.ReminderEntry, now: Date) -> String {
let remaining = max(entry.event.start.timeIntervalSince(now), 0)
let minutes = max(1, Int(ceil(remaining / 60)))
return minutes == 1 ? "in 1 min" : "in \(minutes) min"
}
static func digitalFrameWidth(for text: String) -> CGFloat {
let width = musicMeasureText(text, font: MusicSupplementFont.monospacedDigitSystemFont(ofSize: 15, weight: .semibold))
return max(width + 18, 76)
}
static func minutesFrameWidth(for text: String) -> CGFloat {
let width = musicMeasureText(text, font: MusicSupplementFont.systemFont(ofSize: 13, weight: .semibold))
return max(width + 18, 88)
}
static func ringDiameter(for notchHeight: CGFloat) -> CGFloat {
max(min(notchHeight - 12, 22), 16)
}
}
private func musicMeasureText(_ text: String, font: MusicSupplementFont) -> CGFloat {
guard !text.isEmpty else { return 0 }
let attributes: [NSAttributedString.Key: Any] = [.font: font]
return CGFloat(ceil(NSAttributedString(string: text, attributes: attributes).size().width))
}
struct FullScreenDropDelegate: DropDelegate { struct FullScreenDropDelegate: DropDelegate {
@Binding var isTargeted: Bool @Binding var isTargeted: Bool
let onDrop: () -> Void let onDrop: () -> Void