Fucked up a lot - need to fix

This commit is contained in:
Hariharan Mudaliar 2025-11-01 16:07:45 +05:30
commit eefd951522
6 changed files with 236 additions and 44 deletions

View file

@ -16957,6 +16957,9 @@
} }
} }
} }
},
"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 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." : {

View file

@ -70,7 +70,7 @@ struct NotchTimerView: View {
// Timer icon in center // Timer icon in center
Image(systemName: "timer") Image(systemName: "timer")
.font(.system(size: 32, weight: .medium)) .font(.system(size: 32, weight: .medium))
.foregroundStyle(timerManager.currentColor) .foregroundStyle(openIconColor)
.scaleEffect(timerManager.isRunning && timerManager.isTimerActive ? 1.1 : 1.0) .scaleEffect(timerManager.isRunning && timerManager.isTimerActive ? 1.1 : 1.0)
.animation( .animation(
timerManager.isRunning && timerManager.isTimerActive ? timerManager.isRunning && timerManager.isTimerActive ?
@ -287,6 +287,13 @@ struct NotchTimerView: View {
} }
} }
private extension NotchTimerView {
var openIconColor: Color {
guard timerManager.isTimerActive && timerManager.isRunning else { return .white.opacity(0.9) }
return timerManager.currentColor
}
}
#Preview { #Preview {
NotchTimerView() NotchTimerView()
.environmentObject(DynamicIslandViewModel()) .environmentObject(DynamicIslandViewModel())

View file

@ -1713,6 +1713,7 @@ struct TimerSettings: View {
@Default(.timerIconColorMode) private var colorMode @Default(.timerIconColorMode) private var colorMode
@Default(.timerSolidColor) private var solidColor @Default(.timerSolidColor) private var solidColor
@Default(.timerShowsCountdown) private var showsCountdown @Default(.timerShowsCountdown) private var showsCountdown
@Default(.timerShowsLabel) private var showsLabel
@Default(.timerShowsProgress) private var showsProgress @Default(.timerShowsProgress) private var showsProgress
@Default(.timerProgressStyle) private var progressStyle @Default(.timerProgressStyle) private var progressStyle
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 @AppStorage("customTimerDuration") private var customTimerDuration: Double = 600
@ -1777,6 +1778,7 @@ struct TimerSettings: View {
ColorPicker("Solid colour", selection: $solidColor, supportsOpacity: false) ColorPicker("Solid colour", selection: $solidColor, supportsOpacity: false)
} }
Toggle("Show timer name", isOn: $showsLabel)
Toggle("Show countdown", isOn: $showsCountdown) Toggle("Show countdown", isOn: $showsCountdown)
Toggle("Show progress", isOn: $showsProgress) Toggle("Show progress", isOn: $showsProgress)

View file

@ -9,12 +9,16 @@ import SwiftUI
import Defaults import Defaults
struct TimerIconAnimation: View { struct TimerIconAnimation: View {
@ObservedObject var timerManager = TimerManager.shared
@Default(.timerIconColorMode) private var colorMode
@Default(.timerSolidColor) private var solidColor
@Default(.timerPresets) private var timerPresets
@Default(.accentColor) private var accentColor @Default(.accentColor) private var accentColor
var body: some View { var body: some View {
ZStack { ZStack {
Circle() Circle()
.fill(accentColor.gradient) .fill(resolvedColor.gradient)
.frame(width: 24, height: 24) .frame(width: 24, height: 24)
Image(systemName: "timer") Image(systemName: "timer")
@ -22,6 +26,20 @@ struct TimerIconAnimation: View {
.foregroundStyle(.white) .foregroundStyle(.white)
} }
} }
private var resolvedColor: Color {
guard timerManager.isTimerActive else { return accentColor }
switch colorMode {
case .adaptive:
if let presetId = timerManager.activePresetId,
let preset = timerPresets.first(where: { $0.id == presetId }) {
return preset.color
}
return timerManager.timerColor
case .solid:
return solidColor
}
}
} }
struct TimerIconView: NSViewRepresentable { struct TimerIconView: NSViewRepresentable {

View file

@ -8,26 +8,96 @@
import SwiftUI import SwiftUI
import Defaults import Defaults
#if canImport(AppKit)
import AppKit
typealias PlatformFont = NSFont
#elseif canImport(UIKit)
import UIKit
typealias PlatformFont = UIFont
#endif
struct TimerLiveActivity: View { struct TimerLiveActivity: View {
@EnvironmentObject var vm: DynamicIslandViewModel @EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var timerManager = TimerManager.shared @ObservedObject var timerManager = TimerManager.shared
@State private var isHovering: Bool = false @State private var isHovering: Bool = false
@Default(.timerShowsCountdown) private var showsCountdown @Default(.timerShowsCountdown) private var showsCountdown
@Default(.timerShowsProgress) private var showsProgress @Default(.timerShowsProgress) private var showsProgress
@Default(.timerShowsLabel) private var showsLabel
@Default(.timerProgressStyle) private var progressStyle @Default(.timerProgressStyle) private var progressStyle
@Default(.timerIconColorMode) private var colorMode @Default(.timerIconColorMode) private var colorMode
@Default(.timerSolidColor) private var solidColor @Default(.timerSolidColor) private var solidColor
@Default(.timerPresets) private var timerPresets @Default(.timerPresets) private var timerPresets
private var iconAreaWidth: CGFloat { private var notchContentHeight: CGFloat {
max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)) max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))
} }
private var wingPadding: CGFloat { 18 }
private var ringWrapsIcon: Bool {
showsRingProgress && showsCountdown
}
private var ringOnRight: Bool {
showsRingProgress && !ringWrapsIcon
}
private var iconWidth: CGFloat {
ringWrapsIcon ? max(notchContentHeight, 32) : max(0, notchContentHeight)
}
private var infoContentWidth: CGFloat {
guard showsInfoSection else { return 0 }
let textWidth = min(max(titleTextWidth, 44), 220)
if showsLabel {
return textWidth
} else {
return min(max(notchContentHeight * 1.4, 64), 220)
}
}
private var infoWidth: CGFloat {
guard showsInfoSection else { return 0 }
return infoContentWidth + 18
}
private var leftWingWidth: CGFloat {
var width = iconWidth + wingPadding
if showsInfoSection {
width += 8 + infoWidth
}
return width
}
private var ringWidth: CGFloat {
ringOnRight ? 32 : 0
}
private var rightWingWidth: CGFloat {
var width = wingPadding
if ringOnRight {
width += ringWidth
}
if ringOnRight && showsCountdown {
width += 8
}
if showsCountdown {
width += countdownWidth
}
return width
}
private var titleTextWidth: CGFloat {
measureTextWidth(timerManager.timerName, font: systemFont(size: 12, weight: .medium))
}
private var countdownTextWidth: CGFloat {
measureTextWidth(timerManager.formattedRemainingTime(), font: monospacedDigitFont(size: 13, weight: .semibold))
}
private var countdownWidth: CGFloat { private var countdownWidth: CGFloat {
guard showsCountdown else { return 0 } guard showsCountdown else { return 0 }
let characters = max(timerManager.formattedRemainingTime().count, 4) return max(countdownTextWidth + 20, 80)
let base = CGFloat(characters) * 9.5 + 24
return max(base, 72)
} }
private var clampedProgress: Double { private var clampedProgress: Double {
@ -51,21 +121,70 @@ struct TimerLiveActivity: View {
showsProgress && progressStyle == .bar showsProgress && progressStyle == .bar
} }
private var showsInfoSection: Bool {
showsLabel || (showsBarProgress && !showsCountdown)
}
private var activePresetColor: Color? { private var activePresetColor: Color? {
guard let presetId = timerManager.activePresetId else { return nil } guard let presetId = timerManager.activePresetId else { return nil }
return timerPresets.first { $0.id == presetId }?.color return timerPresets.first { $0.id == presetId }?.color
} }
private func measureTextWidth(_ text: String, font: PlatformFont) -> CGFloat {
let attributes: [NSAttributedString.Key: Any] = [.font: font]
let width = NSAttributedString(string: text, attributes: attributes).size().width
return CGFloat(ceil(width))
}
private func systemFont(size: CGFloat, weight: PlatformFont.Weight) -> PlatformFont {
#if canImport(AppKit)
return NSFont.systemFont(ofSize: size, weight: weight)
#else
return UIFont.systemFont(ofSize: size, weight: weight)
#endif
}
private func monospacedDigitFont(size: CGFloat, weight: PlatformFont.Weight) -> PlatformFont {
#if canImport(AppKit)
return NSFont.monospacedDigitSystemFont(ofSize: size, weight: weight)
#else
return UIFont.monospacedDigitSystemFont(ofSize: size, weight: weight)
#endif
}
var body: some View { var body: some View {
HStack(spacing: 10) { HStack(spacing: 0) {
iconSection Color.clear
infoSection .frame(width: leftWingWidth, height: notchContentHeight)
if showsRingProgress { .background(alignment: .leading) {
ringSection HStack(spacing: showsInfoSection ? 8 : 0) {
} iconSection
if showsCountdown { if showsInfoSection {
countdownSection infoSection
} }
}
.padding(.leading, wingPadding / 2)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
Rectangle()
.fill(.black)
.frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0), height: notchContentHeight)
Color.clear
.frame(width: rightWingWidth, height: notchContentHeight)
.background(alignment: .trailing) {
HStack(spacing: ringOnRight && showsCountdown ? 8 : 0) {
if ringOnRight {
ringSection
}
if showsCountdown {
countdownSection
}
}
.padding(.trailing, wingPadding / 2)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
}
} }
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
.contentShape(Rectangle()) .contentShape(Rectangle())
@ -77,42 +196,81 @@ struct TimerLiveActivity: View {
} }
private var iconSection: some View { private var iconSection: some View {
let iconSize = max(20, iconAreaWidth - 4) let ringDiameter = ringWrapsIcon ? max(iconWidth, 32) : iconWidth
return Image(systemName: "timer") let iconSize = ringWrapsIcon ? max(ringDiameter - 10, 18) : max(20, iconWidth - 4)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(glyphColor) return ZStack {
.frame(width: iconSize, height: iconSize) if ringWrapsIcon {
.frame(width: iconAreaWidth, height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center) Circle()
.stroke(Color.white.opacity(0.15), lineWidth: 3)
.frame(width: ringDiameter, height: ringDiameter)
Circle()
.trim(from: 0, to: clampedProgress)
.stroke(glyphColor, style: StrokeStyle(lineWidth: 3, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.smooth(duration: 0.25), value: clampedProgress)
.frame(width: ringDiameter, height: ringDiameter)
}
Image(systemName: "timer")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(glyphColor)
.frame(width: iconSize, height: iconSize)
}
.frame(width: ringWrapsIcon ? ringDiameter : iconWidth,
height: notchContentHeight,
alignment: .center)
} }
private var infoSection: some View { private var infoSection: some View {
Rectangle() let availableWidth = max(0, infoWidth - 18)
let resolvedTextWidth = min(max(titleTextWidth, 44), availableWidth)
let shouldMarquee = showsLabel && (timerManager.isFinished || timerManager.isOvertime || titleTextWidth > availableWidth)
let showsBarHere = showsBarProgress && !showsCountdown
let barWidth = showsLabel ? resolvedTextWidth : availableWidth
return Rectangle()
.fill(.black) .fill(.black)
.overlay( .frame(width: infoWidth, height: notchContentHeight)
VStack(alignment: .leading, spacing: 4) { .overlay(alignment: .leading) {
if timerManager.isFinished || timerManager.isOvertime { VStack(alignment: .leading, spacing: showsBarHere ? 4 : 0) {
GeometryReader { geo in if showsLabel {
if shouldMarquee {
MarqueeText( MarqueeText(
.constant(timerManager.timerName), .constant(timerManager.timerName),
font: .system(size: 12, weight: .medium),
nsFont: .callout,
textColor: .white, textColor: .white,
minDuration: 0.25, minDuration: 0.25,
frameWidth: geo.size.width - 12 frameWidth: resolvedTextWidth
) )
} else {
Text(timerManager.timerName)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.foregroundStyle(.white)
.transition(.opacity.combined(with: .move(edge: .top)))
.frame(width: resolvedTextWidth, alignment: .leading)
} }
.frame(height: 16) }
} else {
Text(timerManager.timerName) if showsBarHere {
.font(.system(size: 12, weight: .medium)) Capsule()
.lineLimit(1) .fill(Color.white.opacity(0.12))
.foregroundStyle(.white) .frame(width: barWidth, height: 3)
.transition(.opacity.combined(with: .move(edge: .top))) .overlay(alignment: .leading) {
Capsule()
.fill(glyphColor)
.frame(width: barWidth * max(0, CGFloat(clampedProgress)))
.animation(.smooth(duration: 0.25), value: clampedProgress)
}
} }
} }
.padding(.horizontal, 12) .padding(.leading, 12)
.padding(.vertical, 6) .padding(.trailing, 6)
.animation(.smooth, value: timerManager.isFinished) }
) .animation(.smooth, value: timerManager.isFinished)
.frame(width: vm.closedNotchSize.width + (isHovering ? 8 : 0))
} }
private var ringSection: some View { private var ringSection: some View {
@ -126,10 +284,12 @@ struct TimerLiveActivity: View {
.animation(.smooth(duration: 0.25), value: clampedProgress) .animation(.smooth(duration: 0.25), value: clampedProgress)
} }
.frame(width: 26, height: 26) .frame(width: 26, height: 26)
.frame(width: ringWidth, height: notchContentHeight, alignment: .center)
} }
private var countdownSection: some View { private var countdownSection: some View {
VStack(spacing: 4) { let barWidth = max(countdownTextWidth, 1)
return VStack(spacing: 4) {
Text(timerManager.formattedRemainingTime()) Text(timerManager.formattedRemainingTime())
.font(.system(size: 13, weight: .semibold, design: .monospaced)) .font(.system(size: 13, weight: .semibold, design: .monospaced))
.foregroundColor(timerManager.isOvertime ? .red : .white) .foregroundColor(timerManager.isOvertime ? .red : .white)
@ -140,18 +300,19 @@ struct TimerLiveActivity: View {
if showsBarProgress { if showsBarProgress {
Capsule() Capsule()
.fill(Color.white.opacity(0.12)) .fill(Color.white.opacity(0.12))
.frame(height: 3) .frame(width: barWidth, height: 3)
.overlay(alignment: .leading) { .overlay(alignment: .leading) {
Capsule() Capsule()
.fill(glyphColor) .fill(glyphColor)
.frame(width: max(0, countdownWidth - 20) * max(0, CGFloat(clampedProgress))) .frame(width: barWidth * max(0, CGFloat(clampedProgress)))
.animation(.smooth(duration: 0.25), value: clampedProgress) .animation(.smooth(duration: 0.25), value: clampedProgress)
} }
.frame(maxWidth: .infinity, alignment: .trailing)
} }
} }
.padding(.trailing, 8) .padding(.trailing, 8)
.frame(width: countdownWidth, .frame(width: countdownWidth,
height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center) height: notchContentHeight, alignment: .center)
} }
} }

View file

@ -432,6 +432,7 @@ extension Defaults.Keys {
static let timerIconColorMode = Key<TimerIconColorMode>("timerIconColorMode", default: .adaptive) static let timerIconColorMode = Key<TimerIconColorMode>("timerIconColorMode", default: .adaptive)
static let timerSolidColor = Key<Color>("timerSolidColor", default: .blue) static let timerSolidColor = Key<Color>("timerSolidColor", default: .blue)
static let timerShowsCountdown = Key<Bool>("timerShowsCountdown", default: true) static let timerShowsCountdown = Key<Bool>("timerShowsCountdown", default: true)
static let timerShowsLabel = Key<Bool>("timerShowsLabel", default: true)
static let timerShowsProgress = Key<Bool>("timerShowsProgress", default: true) static let timerShowsProgress = Key<Bool>("timerShowsProgress", default: true)
static let timerProgressStyle = Key<TimerProgressStyle>("timerProgressStyle", default: .bar) static let timerProgressStyle = Key<TimerProgressStyle>("timerProgressStyle", default: .bar)