Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 78 additions & 85 deletions LoopFollow/Snoozer/SnoozerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() }
}
Expand All @@ -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 {
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -216,40 +169,82 @@ 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))
.foregroundColor(.white.opacity(0.9))
.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()
}
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion LoopFollow/Storage/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Observable {
var tempTarget = ObservableValue<HKQuantity?>(default: nil)
var override = ObservableValue<String?>(default: nil)

var minAgoText = ObservableValue<String>(default: "?? min ago")
var minAgoText = ObservableValue<String>(default: "?? ago")
var bgText = ObservableValue<String>(default: "BG")
var bg = ObservableValue<Int?>(default: nil)
var bgStale = ObservableValue<Bool>(default: true)
Expand Down
27 changes: 4 additions & 23 deletions LoopFollow/Task/MinAgoTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}
}
Loading