mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
BugFix stuff
This commit is contained in:
parent
67509b01a0
commit
911ed0da21
1 changed files with 302 additions and 107 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue