diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index f70daaf8c..edb0561bd 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -74,11 +74,8 @@ struct SnoozerView: View { if alarm != nil { showSnoozerBar = true cancelAutoHide() - } else { - // When alarm is dismissed, schedule auto-hide if no global snooze is active - if !isGlobalSnoozeActive { - scheduleAutoHide() - } + } else if !isGlobalSnoozeActive { + scheduleAutoHide() } } .onChange(of: isGlobalSnoozeActive) { active in @@ -98,56 +95,6 @@ struct SnoozerView: View { .transition(.move(edge: .top).combined(with: .opacity)) } } - .overlay(alignment: .bottom) { - if let alarm = vm.activeAlarm { - VStack(spacing: 16) { - // Alarm name at the top - Text(alarm.name) - .font(.system(size: 30, weight: .semibold)) - .foregroundColor(.white) - .lineLimit(1) - .minimumScaleFactor(0.5) - .padding(.top, 20) - - Divider() - - // Snooze duration controls - if alarm.type.snoozeTimeUnit != .none { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Snooze for") - .font(.headline) - Text("\(vm.snoozeUnits) \(vm.timeUnitLabel)") - .font(.title3).bold() - } - Spacer() - Stepper("", value: $vm.snoozeUnits, - in: alarm.type.snoozeRange, - step: alarm.type.snoozeStep) - .labelsHidden() - } - .padding(.horizontal, 24) - } - - // Snooze button anchored to tab bar edge (bottom of VStack) - Button(action: vm.snoozeTapped) { - Text(vm.snoozeUnits == 0 ? "Acknowledge" : "Snooze") - .font(.system(size: 30, weight: .bold)) - .frame(maxWidth: .infinity, minHeight: 60) - .background(Color.orange) - .foregroundColor(.white) - .clipShape(Capsule()) - } - .padding(.horizontal, 24) - .padding(.bottom, 20) - } - .background(.ultraThinMaterial) - .cornerRadius(20, corners: [.topLeft, .topRight]) - .transition(.move(edge: .bottom).combined(with: .opacity)) - .animation(.spring(), value: vm.activeAlarm != nil) - .padding(.bottom, 0) // Anchor directly to bottom edge - } - } .sheet(isPresented: $showDatePickerDate) { datePickerSheetDate() } .sheet(isPresented: $showDatePickerTime) { datePickerSheetTime() } } @@ -157,10 +104,16 @@ struct SnoozerView: View { private func leftColumn(isLandscape: Bool, barShowing: Bool) -> some View { let topPad: CGFloat = barShowing ? 0 : 16 - let bigMaxH: CGFloat = barShowing ? (isLandscape ? 210 : 220) : 240 - let dirMaxH: CGFloat = barShowing ? (isLandscape ? 72 : 72) : 80 - let deltaMaxH: CGFloat = barShowing ? (isLandscape ? 60 : 60) : 68 - let ageMaxH: CGFloat = barShowing ? 36 : 40 + let bigMaxH: CGFloat = barShowing ? (isLandscape ? 210 : 210) : 250 + let dirMaxH: CGFloat = barShowing + ? (isLandscape ? 72 : 85) + : (isLandscape ? 80 : 100) + let deltaMaxH: CGFloat = barShowing + ? (isLandscape ? 60 : 105) + : (isLandscape ? 68 : 120) + let ageMaxH: CGFloat = barShowing + ? (isLandscape ? 36 : 105) + : (isLandscape ? 40 : 120) return VStack(spacing: 0) { if !isLandscape && showDisplayName.value { @@ -170,7 +123,7 @@ struct SnoozerView: View { } Text(bgText.value) - .font(.system(size: 300, weight: .black)) + .font(.system(size: 300, weight: .black).monospacedDigit()) .minimumScaleFactor(0.5) .foregroundColor(bgTextColor.value) .strikethrough( @@ -185,29 +138,29 @@ struct SnoozerView: View { Text(directionText.value) .font(.system(size: 90, weight: .black)) Text(deltaText.value) - .font(.system(size: 70)) + .font(.system(size: 70).monospacedDigit()) } .minimumScaleFactor(0.5) .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: dirMaxH) } else { Text(directionText.value) - .font(.system(size: 110, weight: .black)) + .font(.system(size: 200, weight: .black)) .minimumScaleFactor(0.5) .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: dirMaxH) Text(deltaText.value) - .font(.system(size: 70)) + .font(.system(size: 160).monospacedDigit()) .minimumScaleFactor(0.5) - .foregroundColor(.white.opacity(0.8)) + .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: deltaMaxH) } Text(minAgoText.value) - .font(.system(size: 60)) + .font(.system(size: 160).monospacedDigit()) .minimumScaleFactor(0.5) - .foregroundColor(.white.opacity(0.6)) + .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: ageMaxH) } .padding(.top, topPad) @@ -216,7 +169,11 @@ struct SnoozerView: View { private func rightColumn(isLandscape: Bool) -> some View { VStack(spacing: 0) { - Spacer() + if isLandscape { + Spacer() + } else { + Spacer().frame(maxHeight: 10) + } if showDisplayName.value && isLandscape { Text(Bundle.main.displayName) .font(.system(size: 50, weight: .bold)) @@ -224,32 +181,70 @@ struct SnoozerView: View { .padding(.bottom, 8) } - if snoozerEmoji.value { - TimelineView(.periodic(from: .now, by: 1)) { context in - VStack(spacing: 4) { - Text(bgEmoji) - .font(.system(size: 128)) - .minimumScaleFactor(0.5) + if let alarm = vm.activeAlarm { + VStack(spacing: 16) { + Text(alarm.name) + .font(.system(size: 30, weight: .semibold)) + .foregroundColor(.white) + .lineLimit(1) + .minimumScaleFactor(0.5) + .padding(.top, 20) + Divider() + + if alarm.type.snoozeTimeUnit != .none { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Snooze for") + .font(.headline) + Text("\(vm.snoozeUnits) \(vm.timeUnitLabel)") + .font(.title3).bold() + } + Spacer() + Stepper("", value: $vm.snoozeUnits, + in: alarm.type.snoozeRange, + step: alarm.type.snoozeStep) + .labelsHidden() + } + .padding(.horizontal, 24) + } - Text(context.date, format: Date.FormatStyle(date: .omitted, time: .shortened)) - .font(.system(size: 70)) - .minimumScaleFactor(0.5) + Button(action: vm.snoozeTapped) { + Text(vm.snoozeUnits == 0 ? "Acknowledge" : "Snooze") + .font(.system(size: 30, weight: .bold)) + .frame(maxWidth: .infinity, minHeight: 60) + .background(Color.orange) .foregroundColor(.white) - .frame(height: 78) + .clipShape(Capsule()) } + .padding(.horizontal, 24) + .padding(.bottom, 20) } + .background(.ultraThinMaterial) + .cornerRadius(20, corners: [.topLeft, .topRight]) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .animation(.spring(), value: vm.activeAlarm != nil) } else { TimelineView(.periodic(from: .now, by: 1)) { context in VStack(spacing: 4) { + if snoozerEmoji.value { + Text(bgEmoji) + .font(.system(size: 128)) + .minimumScaleFactor(0.5) + } + Text(context.date, format: Date.FormatStyle(date: .omitted, time: .shortened)) - .font(.system(size: 70)) + .font(.system(size: 160)) .minimumScaleFactor(0.5) .foregroundColor(.white) - .frame(height: 78) + .frame(height: 115) } } + if isLandscape { + Spacer() + } else { + Spacer().frame(maxHeight: 15) + } } - Spacer() } } @@ -490,12 +485,10 @@ struct SnoozerView: View { private func scheduleAutoHide() { cancelAutoHide() - // Always schedule the task - it will check conditions when it executes - // This ensures auto-hide works even if conditions change between scheduling and execution + if isGlobalSnoozeActive || vm.activeAlarm != nil { return } let task = DispatchWorkItem { - // Only hide if neither global snooze nor active alarm exists - if !self.isGlobalSnoozeActive && self.vm.activeAlarm == nil { - withAnimation { self.showSnoozerBar = false } + if !isGlobalSnoozeActive && vm.activeAlarm == nil { + withAnimation { showSnoozerBar = false } } } autoHideTask = task diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index f32887523..2089f3c20 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -15,7 +15,7 @@ class Observable { var tempTarget = ObservableValue(default: nil) var override = ObservableValue(default: nil) - var minAgoText = ObservableValue(default: "?? min ago") + var minAgoText = ObservableValue(default: "?? ago") var bgText = ObservableValue(default: "BG") var bg = ObservableValue(default: nil) var bgStale = ObservableValue(default: true) diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 0ea011cee..3dfb664dc 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -34,15 +34,10 @@ extension MainViewController { formatter.zeroFormattingBehavior = .dropLeading let shouldDisplaySeconds = secondsAgo >= 270 && secondsAgo < 720 // 4.5 to 12 minutes - - if shouldDisplaySeconds { - formatter.allowedUnits = [.minute, .second] - } else { - formatter.allowedUnits = [.minute] - } + formatter.allowedUnits = [.minute, .second] let formattedDuration = formatter.string(from: secondsAgo) ?? "" - let minAgoDisplayText = formattedDuration + " min ago" + let minAgoDisplayText = formattedDuration + " ago" // Update UI only if the display text has changed if minAgoDisplayText != Observable.shared.minAgoText.value { @@ -72,21 +67,7 @@ extension MainViewController { // Determine the next run interval based on the current state let nextUpdateInterval: TimeInterval - if shouldDisplaySeconds { - // Update every second when showing seconds - nextUpdateInterval = 1.0 - } else if secondsAgo >= 240, secondsAgo < 720 { - // Schedule exactly at the transition point to start showing seconds - nextUpdateInterval = 270.0 - secondsAgo - } else { - // Schedule exactly at the transition point to next minute - let secondsToNextMinute = 60.0 - (secondsAgo.truncatingRemainder(dividingBy: 60.0)) - nextUpdateInterval = secondsToNextMinute - } - - // Ensure the nextUpdateInterval is not negative or too small - let safeNextInterval = max(nextUpdateInterval, 1.0) - - TaskScheduler.shared.rescheduleTask(id: .minAgoUpdate, to: Date().addingTimeInterval(safeNextInterval)) + nextUpdateInterval = 1.0 + TaskScheduler.shared.rescheduleTask(id: .minAgoUpdate, to: Date().addingTimeInterval(nextUpdateInterval)) } }