Fix Timer Bug for UI Layout

This commit is contained in:
Hariharan Mudaliar 2025-12-21 13:45:38 +05:30
commit 6e365c1b11
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
5 changed files with 100 additions and 67 deletions

View file

@ -767,7 +767,7 @@ struct ContentView: View {
triggerHapticIfAllowed() triggerHapticIfAllowed()
} }
let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive let shouldFocusTimerTab = enableTimerFeature && timerDisplayMode == .tab && timerManager.isTimerActive && !enableMinimalisticUI
guard vm.notchState == .closed, guard vm.notchState == .closed,
!coordinator.sneakPeek.show, !coordinator.sneakPeek.show,

View file

@ -51,6 +51,10 @@ class DynamicIslandViewCoordinator: ObservableObject {
@Published var currentView: NotchViews = .home { @Published var currentView: NotchViews = .home {
didSet { didSet {
if Defaults[.enableMinimalisticUI] && currentView != .home {
currentView = .home
return
}
handleStatsTabTransition(from: oldValue, to: currentView) handleStatsTabTransition(from: oldValue, to: currentView)
} }
} }
@ -116,6 +120,13 @@ class DynamicIslandViewCoordinator: ObservableObject {
self?.handleTimerFeatureToggle(change.newValue) self?.handleTimerFeatureToggle(change.newValue)
} }
.store(in: &cancellables) .store(in: &cancellables)
Defaults.publisher(.enableMinimalisticUI)
.receive(on: DispatchQueue.main)
.sink { [weak self] change in
self?.handleMinimalisticModeChange(change.newValue)
}
.store(in: &cancellables)
} }
private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) { private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) {
@ -152,6 +163,15 @@ class DynamicIslandViewCoordinator: ObservableObject {
currentView = .home currentView = .home
} }
} }
private func handleMinimalisticModeChange(_ isEnabled: Bool) {
guard isEnabled else { return }
if currentView != .home {
withAnimation(.smooth) {
currentView = .home
}
}
}
func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") { func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") {
let resolvedDuration: TimeInterval let resolvedDuration: TimeInterval

View file

@ -11340,10 +11340,6 @@
"comment" : "A description of how to manage the microphone privacy indicator preferences.", "comment" : "A description of how to manage the microphone privacy indicator preferences.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Managed in Clock app" : {
"comment" : "A notice displayed below the custom timer section, explaining that the timer is managed by the Clock app.",
"isCommentAutoGenerated" : true
},
"Mark as complete" : { "Mark as complete" : {
"comment" : "An accessibility label for a button that marks an item as complete.", "comment" : "An accessibility label for a button that marks an item as complete.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true

View file

@ -16,6 +16,7 @@ struct NotchTimerView: View {
@ObservedObject var timerManager = TimerManager.shared @ObservedObject var timerManager = TimerManager.shared
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@Default(.enableTimerFeature) var enableTimerFeature @Default(.enableTimerFeature) var enableTimerFeature
@Default(.enableMinimalisticUI) private var enableMinimalisticUI
@Default(.timerPresets) private var timerPresets @Default(.timerPresets) private var timerPresets
@Default(.timerIconColorMode) private var colorMode @Default(.timerIconColorMode) private var colorMode
@Default(.timerSolidColor) private var solidColor @Default(.timerSolidColor) private var solidColor
@ -75,11 +76,13 @@ struct NotchTimerView: View {
private var leftColumn: some View { private var leftColumn: some View {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {
if timerManager.isTimerActive { if timerManager.isTimerActive {
Spacer(minLength: 0)
activeTimerCard activeTimerCard
Spacer(minLength: 0)
} else { } else {
customTimerComposer customTimerComposer
Spacer(minLength: 0)
} }
Spacer(minLength: 0)
} }
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.frame(maxHeight: maxTabContentHeight, alignment: .top) .frame(maxHeight: maxTabContentHeight, alignment: .top)
@ -104,7 +107,9 @@ struct NotchTimerView: View {
ForEach(timerPresets) { preset in ForEach(timerPresets) { preset in
TimerPresetCard(preset: preset, isActive: timerManager.activePresetId == preset.id) { TimerPresetCard(preset: preset, isActive: timerManager.activePresetId == preset.id) {
timerManager.startTimer(duration: preset.duration, name: preset.name, preset: preset) timerManager.startTimer(duration: preset.duration, name: preset.name, preset: preset)
coordinator.currentView = .timer if !enableMinimalisticUI {
coordinator.currentView = .timer
}
} }
.listRowInsets(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0)) .listRowInsets(EdgeInsets(top: 2, leading: 0, bottom: 2, trailing: 0))
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
@ -146,13 +151,9 @@ struct NotchTimerView: View {
countdownSection countdownSection
} }
if showsProgress { progressSection
progressSection
}
} }
.padding(16) .padding(.horizontal, 4)
.background(Color.white.opacity(0.06))
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
} }
@ViewBuilder @ViewBuilder
@ -186,7 +187,7 @@ struct NotchTimerView: View {
} }
} }
} else { } else {
externalTimerNotice inactiveTimerPlaceholder
} }
} }
@ -218,47 +219,52 @@ struct NotchTimerView: View {
.frame(height: 32) .frame(height: 32)
} }
@ViewBuilder
private var countdownSection: some View { private var countdownSection: some View {
VStack(alignment: .trailing, spacing: 4) { if showsProgress && progressStyle == .ring {
Text(timerManager.formattedRemainingTime()) TimerProgressRing(
.font(.system(size: 36, weight: .black, design: .monospaced)) progress: timerManager.progress,
.foregroundStyle(timerManager.isOvertime ? Color.red : .white) tint: timerAccentColor,
.contentTransition(.numericText()) timeText: timerManager.formattedRemainingTime(),
.animation(.smooth(duration: 0.25), value: timerManager.remainingTime) isOvertime: timerManager.isOvertime,
.lineLimit(1) remainingTime: timerManager.remainingTime
.minimumScaleFactor(0.8) )
} else {
VStack(alignment: .trailing, spacing: 4) {
Text(timerManager.formattedRemainingTime())
.font(.system(size: 36, weight: .black, design: .monospaced))
.foregroundStyle(timerManager.isOvertime ? Color.red : .white)
.contentTransition(.numericText())
.animation(.smooth(duration: 0.25), value: timerManager.remainingTime)
.lineLimit(1)
.minimumScaleFactor(0.8)
if timerManager.isOvertime { if timerManager.isOvertime {
Text("Overtime") Text("Overtime")
.font(.caption) .font(.caption)
.foregroundStyle(.red) .foregroundStyle(.red)
}
} }
.frame(width: 190, alignment: .trailing)
} }
.frame(width: 190, alignment: .trailing)
} }
@ViewBuilder @ViewBuilder
private var progressSection: some View { private var progressSection: some View {
if progressStyle == .bar { if showsProgress && progressStyle == .bar {
GeometryReader { geometry in Capsule()
Capsule() .fill(Color.white.opacity(0.12))
.fill(Color.white.opacity(0.12)) .frame(height: 4)
.frame(height: 4) .overlay(alignment: .leading) {
.overlay(alignment: .leading) { Capsule()
Capsule() .fill(timerAccentColor)
.fill(timerAccentColor) .frame(height: 4)
.frame(width: geometry.size.width * max(0, CGFloat(min(timerManager.progress, 1.0)))) .scaleEffect(x: normalizedProgress, y: 1, anchor: .leading)
.animation(.smooth(duration: 0.25), value: timerManager.progress) .animation(.smooth(duration: 0.25), value: timerManager.progress)
} }
} .frame(maxWidth: .infinity)
.frame(height: 4) .padding(.top, 6)
.frame(maxWidth: .infinity) .padding(.bottom, 2)
} else {
HStack(spacing: 12) {
TimerProgressRing(progress: timerManager.progress, tint: timerAccentColor)
.frame(height: 56)
Spacer()
}
} }
} }
@ -314,18 +320,9 @@ struct NotchTimerView: View {
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
} }
private var externalTimerNotice: some View { private var inactiveTimerPlaceholder: some View {
VStack(alignment: .leading, spacing: 6) { Color.clear
Label("Managed in Clock app", systemImage: "arrow.triangle.2.circlepath") .frame(height: 46)
.font(.caption)
.foregroundStyle(.secondary)
Button("Hide", role: .destructive) {
timerManager.endExternalTimer(triggerSmoothClose: false)
}
.buttonStyle(.borderless)
.font(.caption)
}
} }
private var disabledState: some View { private var disabledState: some View {
@ -383,6 +380,10 @@ struct NotchTimerView: View {
} }
} }
private var normalizedProgress: CGFloat {
CGFloat(max(0, min(timerManager.progress, 1)))
}
private var timerDisplayName: String { private var timerDisplayName: String {
timerManager.timerName.isEmpty ? "Timer" : timerManager.timerName timerManager.timerName.isEmpty ? "Timer" : timerManager.timerName
} }
@ -450,7 +451,9 @@ struct NotchTimerView: View {
Button { Button {
withAnimation(.smooth) { withAnimation(.smooth) {
timerManager.startTimer(duration: customDurationInSeconds, name: "Custom Timer") timerManager.startTimer(duration: customDurationInSeconds, name: "Custom Timer")
coordinator.currentView = .timer if !enableMinimalisticUI {
coordinator.currentView = .timer
}
} }
} label: { } label: {
Label("Start", systemImage: "play.fill") Label("Start", systemImage: "play.fill")
@ -552,18 +555,32 @@ private struct TimerControlButton: View {
private struct TimerProgressRing: View { private struct TimerProgressRing: View {
let progress: Double let progress: Double
let tint: Color let tint: Color
let timeText: String
let isOvertime: Bool
let remainingTime: TimeInterval
private var clampedProgress: Double { min(max(progress, 0), 1) }
var body: some View { var body: some View {
ZStack { ZStack {
Circle() Circle()
.stroke(Color.white.opacity(0.12), lineWidth: 6) .stroke(Color.white.opacity(0.12), lineWidth: 8)
Circle() Circle()
.trim(from: 0, to: min(progress, 1.0)) .trim(from: 0, to: clampedProgress)
.stroke(tint, style: StrokeStyle(lineWidth: 6, lineCap: .round)) .stroke(tint, style: StrokeStyle(lineWidth: 8, lineCap: .round))
.rotationEffect(.degrees(-90)) .rotationEffect(.degrees(-90))
.animation(.smooth(duration: 0.3), value: progress) .animation(.smooth(duration: 0.3), value: clampedProgress)
Text(timeText)
.font(.system(size: 28, weight: .black, design: .monospaced))
.foregroundStyle(isOvertime ? Color.red : .white)
.minimumScaleFactor(0.6)
.lineLimit(1)
.contentTransition(.numericText())
.animation(.smooth(duration: 0.25), value: remainingTime)
} }
.frame(width: 60, height: 60) .frame(width: 110, height: 110)
} }
} }

View file

@ -284,7 +284,7 @@ class DynamicIslandViewModel: NSObject, ObservableObject {
// Set the current view to shelf if it contains files and the user enables openShelfByDefault // Set the current view to shelf if it contains files and the user enables openShelfByDefault
// Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home
if !ShelfStateViewModel.shared.isEmpty && Defaults[.openShelfByDefault] { if !ShelfStateViewModel.shared.isEmpty && Defaults[.openShelfByDefault] && !Defaults[.enableMinimalisticUI] {
coordinator.currentView = .shelf coordinator.currentView = .shelf
} else if !coordinator.openLastTabByDefault { } else if !coordinator.openLastTabByDefault {
coordinator.currentView = .home coordinator.currentView = .home