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 SwiftUI
|
||||
import SwiftUIIntrospect
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#elseif canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
struct ContentView: View {
|
||||
|
|
@ -529,8 +534,6 @@ struct ContentView: View {
|
|||
} else if vm.notchState == .open {
|
||||
DynamicIslandHeader()
|
||||
.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 {
|
||||
Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight)
|
||||
}
|
||||
|
|
@ -688,12 +691,23 @@ struct ContentView: View {
|
|||
let secondary = resolveMusicSecondaryLiveActivity()
|
||||
let notchContentHeight = max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
|
||||
let wingBaseWidth = max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2)
|
||||
let centerBaseWidth = 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 rightWingWidth = resolvedRightWingWidth(for: secondary, baseWidth: wingBaseWidth, centerBaseWidth: centerBaseWidth)
|
||||
let centerWidth = resolvedCenterWidth(baseWidth: centerBaseWidth, baseRightWidth: wingBaseWidth, desiredRightWidth: rightWingWidth)
|
||||
let rawCenterBaseWidth = vm.closedNotchSize.width + (isHovering ? 8 : 0)
|
||||
let centerBaseWidth = max(rawCenterBaseWidth, 96)
|
||||
let inlineSneakPeekActive = (
|
||||
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 effectiveCenterWidth = inlineSneakPeekActive ? 380 : centerWidth
|
||||
|
||||
HStack(spacing: 0) {
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
|
|
@ -729,7 +743,6 @@ struct ContentView: View {
|
|||
)
|
||||
.opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
|
||||
Spacer(minLength: vm.closedNotchSize.width)
|
||||
// Song Artist
|
||||
Text(musicManager.artistName)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
|
@ -744,7 +757,6 @@ struct ContentView: View {
|
|||
)
|
||||
.opacity((coordinator.expandingView.show && Defaults[.enableSneakPeek] && Defaults[.sneakPeekStyles] == .inline) ? 1 : 0)
|
||||
Spacer(minLength: vm.closedNotchSize.width)
|
||||
// Timer Status
|
||||
Text(timerManager.formattedRemainingTime())
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
|
@ -754,11 +766,12 @@ struct ContentView: View {
|
|||
}
|
||||
)
|
||||
|
||||
musicRightWing(for: secondary)
|
||||
musicRightWing(for: secondary, notchHeight: notchContentHeight)
|
||||
.frame(width: rightWingWidth, height: notchContentHeight, alignment: .center)
|
||||
.id(secondary?.id ?? "music-spectrum")
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
.frame(width: notchWidth, height: notchContentHeight)
|
||||
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
||||
.animation(.smooth(duration: 0.25), value: secondary?.id)
|
||||
}
|
||||
|
|
@ -792,32 +805,81 @@ struct ContentView: View {
|
|||
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 }
|
||||
|
||||
func scaledWidth(factor: CGFloat, extra: CGFloat = 0) -> CGFloat {
|
||||
max(baseWidth, max(centerBaseWidth * factor, baseWidth + extra))
|
||||
}
|
||||
|
||||
switch secondary {
|
||||
case .timer:
|
||||
return scaledWidth(factor: 0.58, extra: 28)
|
||||
case .reminder:
|
||||
return scaledWidth(factor: 0.52, extra: 22)
|
||||
return timerRightWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth)
|
||||
case .reminder(let entry):
|
||||
return reminderRightWingWidth(for: entry, baseWidth: baseWidth, notchHeight: notchHeight, now: reminderManager.currentDate)
|
||||
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:
|
||||
return scaledWidth(factor: 0.38, extra: 8)
|
||||
return scaledWingWidth(baseWidth: baseWidth, centerBaseWidth: centerBaseWidth, factor: 0.36, extra: 8)
|
||||
case .focus:
|
||||
return scaledWidth(factor: 0.42, extra: 10)
|
||||
return focusRightWingWidth(baseWidth: baseWidth)
|
||||
case .recording:
|
||||
return scaledWidth(factor: 0.45, extra: 14)
|
||||
return recordingRightWingWidth(baseWidth: baseWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedCenterWidth(baseWidth: CGFloat, baseRightWidth: CGFloat, desiredRightWidth: CGFloat) -> CGFloat {
|
||||
let delta = max(0, desiredRightWidth - baseRightWidth)
|
||||
return max(96, baseWidth - delta)
|
||||
private func timerRightWingWidth(baseWidth: CGFloat, centerBaseWidth: CGFloat) -> CGFloat {
|
||||
if timerShowsCountdown {
|
||||
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
|
||||
|
|
@ -834,7 +896,7 @@ struct ContentView: View {
|
|||
.foregroundStyle(timerAccentColor)
|
||||
case .reminder(let entry):
|
||||
let accent = reminderColor(for: entry, now: reminderManager.currentDate)
|
||||
Image(systemName: "calendar.badge.clock")
|
||||
Image(systemName: "clock")
|
||||
.font(.system(size: badgeSize * 0.55, weight: .semibold))
|
||||
.foregroundStyle(accent)
|
||||
case .focus(let mode):
|
||||
|
|
@ -866,7 +928,7 @@ struct ContentView: View {
|
|||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func musicRightWing(for secondary: MusicSecondaryLiveActivity?) -> some View {
|
||||
private func musicRightWing(for secondary: MusicSecondaryLiveActivity?, notchHeight: CGFloat) -> some View {
|
||||
switch secondary {
|
||||
case .timer:
|
||||
MusicTimerSupplementView(
|
||||
|
|
@ -874,14 +936,16 @@ struct ContentView: View {
|
|||
accentColor: timerAccentColor,
|
||||
showsCountdown: timerShowsCountdown,
|
||||
showsProgress: timerShowsProgress,
|
||||
progressStyle: timerProgressStyle
|
||||
progressStyle: timerProgressStyle,
|
||||
notchHeight: notchHeight
|
||||
)
|
||||
case .reminder(let entry):
|
||||
MusicReminderSupplementView(
|
||||
entry: entry,
|
||||
now: reminderManager.currentDate,
|
||||
style: reminderPresentationStyle,
|
||||
accent: reminderColor(for: entry, now: reminderManager.currentDate)
|
||||
accent: reminderColor(for: entry, now: reminderManager.currentDate),
|
||||
notchHeight: notchHeight
|
||||
)
|
||||
case .capsLock(let showLabel):
|
||||
if showLabel {
|
||||
|
|
@ -893,15 +957,17 @@ struct ContentView: View {
|
|||
IdleAnimationView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.horizontal, 4)
|
||||
case .focus, .recording:
|
||||
case .focus(let mode):
|
||||
spectrumView(forceSpectrum: true)
|
||||
case .recording:
|
||||
spectrumView(forceSpectrum: true, trailingInset: 6)
|
||||
case .none:
|
||||
spectrumView(forceSpectrum: false)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func spectrumView(forceSpectrum: Bool) -> some View {
|
||||
private func spectrumView(forceSpectrum: Bool, trailingInset: CGFloat = 0) -> some View {
|
||||
if useMusicVisualizer || forceSpectrum {
|
||||
Rectangle()
|
||||
.fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient)
|
||||
|
|
@ -912,6 +978,7 @@ struct ContentView: View {
|
|||
.frame(width: 16, height: 12)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(.trailing, trailingInset)
|
||||
} else {
|
||||
LottieAnimationView()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
|
|
@ -1462,15 +1529,75 @@ private struct MusicTimerSupplementView: View {
|
|||
let showsCountdown: Bool
|
||||
let showsProgress: Bool
|
||||
let progressStyle: TimerProgressStyle
|
||||
let notchHeight: CGFloat
|
||||
|
||||
private var clampedProgress: Double {
|
||||
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 {
|
||||
HStack(spacing: 10) {
|
||||
if showsProgress {
|
||||
if progressStyle == .ring {
|
||||
HStack(spacing: showsRingProgress && showsCountdown ? 8 : 0) {
|
||||
if showsRingProgress {
|
||||
ringView
|
||||
}
|
||||
|
||||
if showsCountdown {
|
||||
countdownStack
|
||||
} else if showsBarProgress {
|
||||
standaloneBarView
|
||||
} else {
|
||||
timerNameView
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -1480,47 +1607,35 @@ private struct MusicTimerSupplementView: View {
|
|||
.rotationEffect(.degrees(-90))
|
||||
.animation(.smooth(duration: 0.25), value: clampedProgress)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
} else if !showsCountdown {
|
||||
barView()
|
||||
}
|
||||
.frame(width: ringDiameter, height: ringDiameter)
|
||||
.frame(width: max(ringDiameter + 4, 30), height: notchHeight, alignment: .center)
|
||||
}
|
||||
|
||||
if showsCountdown {
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(timerManager.formattedRemainingTime())
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(timerManager.isOvertime ? .red : .white)
|
||||
.contentTransition(.numericText())
|
||||
.animation(.smooth(duration: 0.25), value: timerManager.remainingTime)
|
||||
|
||||
if showsProgress && progressStyle == .bar {
|
||||
barView()
|
||||
}
|
||||
}
|
||||
private var standaloneBarView: some View {
|
||||
barView(width: 68)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
} else if !showsProgress {
|
||||
}
|
||||
|
||||
private var timerNameView: some View {
|
||||
Text(timerManager.timerName)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.frame(width: timerNameFrameWidth, alignment: .trailing)
|
||||
}
|
||||
|
||||
private func barView() -> some View {
|
||||
private func barView(width: CGFloat) -> some View {
|
||||
Capsule()
|
||||
.fill(Color.white.opacity(0.15))
|
||||
.frame(width: 68, height: 4)
|
||||
.frame(width: width, height: 4)
|
||||
.overlay(alignment: .leading) {
|
||||
Capsule()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private struct MusicReminderSupplementView: View {
|
||||
|
|
@ -1528,31 +1643,23 @@ private struct MusicReminderSupplementView: View {
|
|||
let now: Date
|
||||
let style: ReminderPresentationStyle
|
||||
let accent: Color
|
||||
|
||||
private var progressValue: Double {
|
||||
guard entry.leadTime > 0 else { return 1 }
|
||||
let remaining = max(entry.event.start.timeIntervalSince(now), 0)
|
||||
let elapsed = entry.leadTime - remaining
|
||||
return min(max(elapsed / entry.leadTime, 0), 1)
|
||||
}
|
||||
|
||||
private var digitalCountdownText: 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)
|
||||
}
|
||||
|
||||
private var minutesCountdownText: 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"
|
||||
}
|
||||
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)
|
||||
|
|
@ -1562,23 +1669,53 @@ private struct MusicReminderSupplementView: View {
|
|||
.rotationEffect(.degrees(-90))
|
||||
.animation(.smooth(duration: 0.25), value: progressValue)
|
||||
}
|
||||
.frame(width: 24, height: 24)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.frame(width: ringDiameter, height: ringDiameter)
|
||||
.frame(width: max(ringDiameter + 4, 26), height: notchHeight, alignment: .center)
|
||||
}
|
||||
|
||||
case .digital:
|
||||
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(maxWidth: .infinity, alignment: .trailing)
|
||||
.frame(width: digitalFrameWidth, alignment: .trailing)
|
||||
.frame(height: notchHeight, alignment: .center)
|
||||
}
|
||||
|
||||
case .minutes:
|
||||
private var minutesCountdownView: some View {
|
||||
Text(minutesCountdownText)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(accent)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.frame(width: minutesFrameWidth, alignment: .trailing)
|
||||
.frame(height: notchHeight, alignment: .center)
|
||||
}
|
||||
|
||||
private var progressValue: Double {
|
||||
guard entry.leadTime > 0 else { return 1 }
|
||||
let remaining = max(entry.event.start.timeIntervalSince(now), 0)
|
||||
let elapsed = entry.leadTime - remaining
|
||||
return min(max(elapsed / entry.leadTime, 0), 1)
|
||||
}
|
||||
|
||||
private var digitalCountdownText: String {
|
||||
ReminderSupplementMetrics.digitalCountdownText(for: entry, now: now)
|
||||
}
|
||||
|
||||
private var minutesCountdownText: String {
|
||||
ReminderSupplementMetrics.minutesCountdownText(for: entry, now: now)
|
||||
}
|
||||
|
||||
private var ringDiameter: CGFloat {
|
||||
ReminderSupplementMetrics.ringDiameter(for: notchHeight)
|
||||
}
|
||||
|
||||
private var digitalFrameWidth: CGFloat {
|
||||
ReminderSupplementMetrics.digitalFrameWidth(for: digitalCountdownText)
|
||||
}
|
||||
|
||||
private var minutesFrameWidth: CGFloat {
|
||||
ReminderSupplementMetrics.minutesFrameWidth(for: minutesCountdownText)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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 {
|
||||
@Binding var isTargeted: Bool
|
||||
let onDrop: () -> Void
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue