mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Fix Timer Bug for UI Layout
This commit is contained in:
parent
d4886dfc9e
commit
6e365c1b11
5 changed files with 100 additions and 67 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue