|
|
@@ -16,36 +16,100 @@ struct SnoozerView: View {
|
|
|
@ObservedObject var bg = Observable.shared.bg
|
|
|
@ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji
|
|
|
|
|
|
+ @ObservedObject private var cfgStore = Storage.shared.alarmConfiguration
|
|
|
+
|
|
|
+ // Snoozer Bar state
|
|
|
+ @State private var showSnoozerBar: Bool = false
|
|
|
+ @State private var showDatePickerDate: Bool = false
|
|
|
+ @State private var showDatePickerTime: Bool = false
|
|
|
+ @State private var autoHideTask: DispatchWorkItem? = nil
|
|
|
+ @State private var lastActiveState: Bool = false
|
|
|
+
|
|
|
+ private var isGlobalSnoozeActive: Bool {
|
|
|
+ if let until = cfgStore.value.snoozeUntil { return until > Date() }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
var body: some View {
|
|
|
GeometryReader { geo in
|
|
|
- ZStack {
|
|
|
- Color.black
|
|
|
- .edgesIgnoringSafeArea(.all)
|
|
|
+ let isLandscape = geo.size.width > geo.size.height
|
|
|
+ let barShowing = showSnoozerBar || isGlobalSnoozeActive
|
|
|
+ let landscapeScale: CGFloat = 0.8
|
|
|
|
|
|
- let isLandscape = geo.size.width > geo.size.height
|
|
|
+ ZStack {
|
|
|
+ Color.black.ignoresSafeArea()
|
|
|
|
|
|
- Group {
|
|
|
+ VStack(spacing: 0) {
|
|
|
if isLandscape {
|
|
|
HStack(spacing: 0) {
|
|
|
- leftColumn(isLandscape: true)
|
|
|
+ leftColumn(isLandscape: true, barShowing: barShowing)
|
|
|
rightColumn(isLandscape: true)
|
|
|
}
|
|
|
} else {
|
|
|
VStack(spacing: 0) {
|
|
|
- leftColumn(isLandscape: false)
|
|
|
+ leftColumn(isLandscape: false, barShowing: barShowing)
|
|
|
rightColumn(isLandscape: false)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- .frame(width: geo.size.width, height: geo.size.height)
|
|
|
+ .contentShape(Rectangle())
|
|
|
+ .onTapGesture { presentSnoozerBar() }
|
|
|
+ .onAppear {
|
|
|
+ presentSnoozerBar()
|
|
|
+ lastActiveState = isGlobalSnoozeActive
|
|
|
+ }
|
|
|
+ .onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
|
|
|
+ let active = isGlobalSnoozeActive
|
|
|
+ if lastActiveState != active {
|
|
|
+ lastActiveState = active
|
|
|
+ if active {
|
|
|
+ showSnoozerBar = true
|
|
|
+ cancelAutoHide()
|
|
|
+ } else {
|
|
|
+ scheduleAutoHide()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .onReceive(vm.$activeAlarm) { alarm in
|
|
|
+ if alarm != nil {
|
|
|
+ showSnoozerBar = true
|
|
|
+ cancelAutoHide()
|
|
|
+ } else if !isGlobalSnoozeActive {
|
|
|
+ scheduleAutoHide()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .onChange(of: isGlobalSnoozeActive) { active in
|
|
|
+ if active {
|
|
|
+ showSnoozerBar = true
|
|
|
+ cancelAutoHide()
|
|
|
+ } else {
|
|
|
+ scheduleAutoHide()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .scaleEffect((isLandscape && barShowing) ? landscapeScale : 1.0, anchor: .top)
|
|
|
+ .animation(.easeOut(duration: 0.18), value: barShowing)
|
|
|
}
|
|
|
+ .safeAreaInset(edge: .top) {
|
|
|
+ if showSnoozerBar || isGlobalSnoozeActive {
|
|
|
+ snoozerBar(compact: isLandscape)
|
|
|
+ .transition(.move(edge: .top).combined(with: .opacity))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .sheet(isPresented: $showDatePickerDate) { datePickerSheetDate() }
|
|
|
+ .sheet(isPresented: $showDatePickerTime) { datePickerSheetTime() }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // MARK: - Left Column (BG / Direction / Delta / Age)
|
|
|
+ // MARK: - Columns
|
|
|
|
|
|
- private func leftColumn(isLandscape: Bool) -> some View {
|
|
|
- VStack(spacing: 0) {
|
|
|
+ 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
|
|
|
+
|
|
|
+ return VStack(spacing: 0) {
|
|
|
if !isLandscape && showDisplayName.value {
|
|
|
Text(Bundle.main.displayName)
|
|
|
.font(.system(size: 50, weight: .bold))
|
|
|
@@ -61,46 +125,42 @@ struct SnoozerView: View {
|
|
|
pattern: .solid,
|
|
|
color: bgStale.value ? .red : .clear
|
|
|
)
|
|
|
- .frame(maxWidth: .infinity, maxHeight: 240)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: bigMaxH)
|
|
|
|
|
|
if isLandscape {
|
|
|
HStack(alignment: .firstTextBaseline, spacing: 20) {
|
|
|
Text(directionText.value)
|
|
|
.font(.system(size: 90, weight: .black))
|
|
|
-
|
|
|
Text(deltaText.value)
|
|
|
.font(.system(size: 70))
|
|
|
}
|
|
|
.minimumScaleFactor(0.5)
|
|
|
.foregroundColor(.white)
|
|
|
- .frame(maxWidth: .infinity, maxHeight: 80)
|
|
|
-
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: dirMaxH)
|
|
|
} else {
|
|
|
Text(directionText.value)
|
|
|
.font(.system(size: 110, weight: .black))
|
|
|
.minimumScaleFactor(0.5)
|
|
|
.foregroundColor(.white)
|
|
|
- .frame(maxWidth: .infinity, maxHeight: 80)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: dirMaxH)
|
|
|
|
|
|
Text(deltaText.value)
|
|
|
.font(.system(size: 70))
|
|
|
.minimumScaleFactor(0.5)
|
|
|
.foregroundColor(.white.opacity(0.8))
|
|
|
- .frame(maxWidth: .infinity, maxHeight: 68)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: deltaMaxH)
|
|
|
}
|
|
|
|
|
|
Text(minAgoText.value)
|
|
|
.font(.system(size: 60))
|
|
|
.minimumScaleFactor(0.5)
|
|
|
.foregroundColor(.white.opacity(0.6))
|
|
|
- .frame(maxWidth: .infinity, maxHeight: 40)
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: ageMaxH)
|
|
|
}
|
|
|
- .padding(.top, 16)
|
|
|
+ .padding(.top, topPad)
|
|
|
.padding(.horizontal, 16)
|
|
|
}
|
|
|
|
|
|
- // MARK: - Right Column (Clock/Alert + Snooze Controls)
|
|
|
-
|
|
|
private func rightColumn(isLandscape: Bool) -> some View {
|
|
|
VStack(spacing: 0) {
|
|
|
Spacer()
|
|
|
@@ -121,7 +181,6 @@ struct SnoozerView: View {
|
|
|
.padding(.top, 20)
|
|
|
Divider()
|
|
|
|
|
|
- // snooze controls
|
|
|
if alarm.type.snoozeTimeUnit != .none {
|
|
|
HStack {
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
@@ -176,18 +235,10 @@ struct SnoozerView: View {
|
|
|
}
|
|
|
|
|
|
private var bgEmoji: String {
|
|
|
- guard let bg = bg.value, !bgStale.value else {
|
|
|
- return "🤷"
|
|
|
- }
|
|
|
-
|
|
|
- if Localizer.getPreferredUnit() == .millimolesPerLiter, Localizer.removePeriodAndCommaForBadge(bgText.value) == "55" {
|
|
|
- return "🦄"
|
|
|
- }
|
|
|
-
|
|
|
- if Localizer.getPreferredUnit() == .milligramsPerDeciliter, bg == 100 {
|
|
|
- return "🦄"
|
|
|
- }
|
|
|
-
|
|
|
+ guard let bg = bg.value, !bgStale.value else { return "🤷" }
|
|
|
+ if Localizer.getPreferredUnit() == .millimolesPerLiter,
|
|
|
+ Localizer.removePeriodAndCommaForBadge(bgText.value) == "55" { return "🦄" }
|
|
|
+ if Localizer.getPreferredUnit() == .milligramsPerDeciliter, bg == 100 { return "🦄" }
|
|
|
switch bg {
|
|
|
case ..<40: return "❌"
|
|
|
case ..<55: return "🥶"
|
|
|
@@ -212,6 +263,358 @@ struct SnoozerView: View {
|
|
|
default: return "👿"
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ // MARK: - Snoozer Bar
|
|
|
+
|
|
|
+ private func snoozerBar(compact: Bool) -> some View {
|
|
|
+ let active = isGlobalSnoozeActive
|
|
|
+ let until = cfgStore.value.snoozeUntil
|
|
|
+ let vPad: CGFloat = compact ? 6 : 10
|
|
|
+ let controlH: CGFloat = compact ? 40 : 44
|
|
|
+ let primaryH: CGFloat = compact ? 48 : 54
|
|
|
+ let primaryMinW: CGFloat = compact ? 210 : 230
|
|
|
+
|
|
|
+ return VStack(spacing: compact ? 6 : 10) {
|
|
|
+ if active {
|
|
|
+ if compact {
|
|
|
+ HStack(spacing: 10) {
|
|
|
+ Image(systemName: "bell.slash.fill")
|
|
|
+ .font(.system(size: 22, weight: .bold))
|
|
|
+ .foregroundColor(.red)
|
|
|
+
|
|
|
+ Text("All alerts snoozed")
|
|
|
+ .font(.headline)
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .lineLimit(1)
|
|
|
+ .minimumScaleFactor(0.8)
|
|
|
+ .layoutPriority(1)
|
|
|
+
|
|
|
+ Spacer(minLength: 6)
|
|
|
+
|
|
|
+ Button(action: { showDatePickerDate = true }) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: "calendar").font(.system(size: 12, weight: .semibold))
|
|
|
+ Text((until ?? Date().addingTimeInterval(3600)).formatted(date: .abbreviated, time: .omitted))
|
|
|
+ .font(.footnote)
|
|
|
+ }
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ .padding(.vertical, 6).padding(.horizontal, 10)
|
|
|
+ .background(Color.white.opacity(0.08))
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(action: { showDatePickerTime = true }) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: "clock").font(.system(size: 12, weight: .semibold))
|
|
|
+ Text((until ?? Date().addingTimeInterval(3600)).formatted(date: .omitted, time: .shortened))
|
|
|
+ .font(.footnote)
|
|
|
+ }
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ .padding(.vertical, 6).padding(.horizontal, 10)
|
|
|
+ .background(Color.white.opacity(0.08))
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(action: { adjustSnooze(byMinutes: -30) }) {
|
|
|
+ Text("− 30m").bold()
|
|
|
+ .frame(minWidth: 76, minHeight: controlH)
|
|
|
+ .background(Color.white.opacity(0.12))
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(action: { adjustSnooze(byMinutes: +30) }) {
|
|
|
+ Text("+ 30m").bold()
|
|
|
+ .frame(minWidth: 76, minHeight: controlH)
|
|
|
+ .background(Color.white.opacity(0.12))
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(role: .destructive, action: { endSnooze() }) {
|
|
|
+ Text("End now").bold()
|
|
|
+ .frame(minWidth: 96, minHeight: controlH)
|
|
|
+ .background(Color.red.opacity(0.6))
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Image(systemName: phaseIconName())
|
|
|
+ .font(.system(size: 18, weight: .semibold))
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ }
|
|
|
+ .padding(.bottom, 2)
|
|
|
+ } else {
|
|
|
+ HStack(alignment: .center, spacing: 14) {
|
|
|
+ Image(systemName: "bell.slash.fill")
|
|
|
+ .font(.system(size: 24, weight: .bold))
|
|
|
+ .foregroundColor(.red)
|
|
|
+
|
|
|
+ VStack(alignment: .leading, spacing: 2) {
|
|
|
+ Text("All alerts snoozed")
|
|
|
+ .font(.headline)
|
|
|
+ .foregroundColor(.white)
|
|
|
+
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Button(action: { showDatePickerDate = true }) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: "calendar").font(.system(size: 12, weight: .semibold))
|
|
|
+ Text((until ?? Date().addingTimeInterval(3600)).formatted(date: .abbreviated, time: .omitted))
|
|
|
+ .font(.subheadline)
|
|
|
+ }
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ .padding(.vertical, 6).padding(.horizontal, 10)
|
|
|
+ .background(Color.white.opacity(0.08))
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(action: { showDatePickerTime = true }) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: "clock").font(.system(size: 12, weight: .semibold))
|
|
|
+ Text((until ?? Date().addingTimeInterval(3600)).formatted(date: .omitted, time: .shortened))
|
|
|
+ .font(.subheadline)
|
|
|
+ }
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ .padding(.vertical, 6).padding(.horizontal, 10)
|
|
|
+ .background(Color.white.opacity(0.08))
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Spacer()
|
|
|
+
|
|
|
+ Image(systemName: phaseIconName())
|
|
|
+ .font(.system(size: 18, weight: .semibold))
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ }
|
|
|
+
|
|
|
+ HStack(spacing: 12) {
|
|
|
+ Button(action: { adjustSnooze(byMinutes: -30) }) {
|
|
|
+ Text("− 30m").font(.title3).bold()
|
|
|
+ .frame(minWidth: 90, minHeight: 44)
|
|
|
+ .background(Color.white.opacity(0.12))
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(action: { adjustSnooze(byMinutes: +30) }) {
|
|
|
+ Text("+ 30m").font(.title3).bold()
|
|
|
+ .frame(minWidth: 90, minHeight: 44)
|
|
|
+ .background(Color.white.opacity(0.12))
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Button(role: .destructive, action: { endSnooze() }) {
|
|
|
+ Text("End now").font(.title3).bold()
|
|
|
+ .frame(minWidth: 110, minHeight: 44)
|
|
|
+ .background(Color.red.opacity(0.6))
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+ }
|
|
|
+ .padding(.bottom, 8)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ HStack(spacing: 12) {
|
|
|
+ Button(action: { activateSnooze1h() }) {
|
|
|
+ HStack(spacing: 10) {
|
|
|
+ Image(systemName: "bell.slash")
|
|
|
+ Text("Snooze all · 1h").bold()
|
|
|
+ .lineLimit(1)
|
|
|
+ .minimumScaleFactor(0.9)
|
|
|
+ }
|
|
|
+ .font(.title3)
|
|
|
+ .frame(minWidth: primaryMinW, minHeight: primaryH)
|
|
|
+ .padding(.horizontal, 6)
|
|
|
+ .background(Color.orange)
|
|
|
+ .foregroundColor(.white)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ .overlay(Capsule().stroke(Color.white.opacity(0.15), lineWidth: 1))
|
|
|
+ .shadow(radius: 3)
|
|
|
+ }.buttonStyle(.plain)
|
|
|
+
|
|
|
+ Spacer()
|
|
|
+
|
|
|
+ Image(systemName: phaseIconName())
|
|
|
+ .font(.system(size: 18, weight: .semibold))
|
|
|
+ .foregroundColor(.white.opacity(0.9))
|
|
|
+ }
|
|
|
+ .padding(.bottom, compact ? 6 : 8)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .padding(.horizontal, 16)
|
|
|
+ .padding(.vertical, vPad)
|
|
|
+ .background(
|
|
|
+ Color.white.opacity(0.08)
|
|
|
+ .cornerRadius(18, corners: [.bottomLeft, .bottomRight])
|
|
|
+ )
|
|
|
+ .onTapGesture { resetAutoHide() }
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Snoozer Bar helpers
|
|
|
+
|
|
|
+ private func presentSnoozerBar() {
|
|
|
+ showSnoozerBar = true
|
|
|
+ if isGlobalSnoozeActive || vm.activeAlarm != nil {
|
|
|
+ cancelAutoHide()
|
|
|
+ } else {
|
|
|
+ scheduleAutoHide()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func cancelAutoHide() {
|
|
|
+ autoHideTask?.cancel()
|
|
|
+ autoHideTask = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ private func scheduleAutoHide() {
|
|
|
+ cancelAutoHide()
|
|
|
+ if isGlobalSnoozeActive || vm.activeAlarm != nil { return }
|
|
|
+ let task = DispatchWorkItem {
|
|
|
+ if !isGlobalSnoozeActive && vm.activeAlarm == nil {
|
|
|
+ withAnimation { showSnoozerBar = false }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ autoHideTask = task
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: task)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func resetAutoHide() {
|
|
|
+ if !isGlobalSnoozeActive, vm.activeAlarm == nil {
|
|
|
+ scheduleAutoHide()
|
|
|
+ } else {
|
|
|
+ cancelAutoHide()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func activateSnooze1h() {
|
|
|
+ if vm.activeAlarm != nil {
|
|
|
+ vm.snoozeTapped()
|
|
|
+ }
|
|
|
+
|
|
|
+ cfgStore.value.snoozeUntil = Date().addingTimeInterval(3600)
|
|
|
+
|
|
|
+ showSnoozerBar = true
|
|
|
+ cancelAutoHide()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func endSnooze() {
|
|
|
+ cfgStore.value.snoozeUntil = nil
|
|
|
+ if vm.activeAlarm == nil {
|
|
|
+ scheduleAutoHide()
|
|
|
+ } else {
|
|
|
+ cancelAutoHide()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func adjustSnooze(byMinutes delta: Int) {
|
|
|
+ guard let current = cfgStore.value.snoozeUntil else { return }
|
|
|
+ let newDate = current.addingTimeInterval(TimeInterval(delta * 60))
|
|
|
+ if newDate <= Date() { endSnooze() } else { cfgStore.value.snoozeUntil = newDate }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func snoozeUntilBindingForDate() -> Binding<Date> {
|
|
|
+ Binding<Date>(
|
|
|
+ get: { cfgStore.value.snoozeUntil ?? Date().addingTimeInterval(3600) },
|
|
|
+ set: { newDateOnly in
|
|
|
+ let base = cfgStore.value.snoozeUntil ?? Date().addingTimeInterval(3600)
|
|
|
+ let cal = Calendar.current
|
|
|
+ let time = cal.dateComponents([.hour, .minute, .second], from: base)
|
|
|
+ var comps = cal.dateComponents([.year, .month, .day], from: newDateOnly)
|
|
|
+ comps.hour = time.hour; comps.minute = time.minute; comps.second = time.second
|
|
|
+ cfgStore.value.snoozeUntil = cal.date(from: comps) ?? newDateOnly
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private func snoozeUntilBindingForTime() -> Binding<Date> {
|
|
|
+ Binding<Date>(
|
|
|
+ get: { cfgStore.value.snoozeUntil ?? Date().addingTimeInterval(3600) },
|
|
|
+ set: { newTimeOnly in
|
|
|
+ let base = cfgStore.value.snoozeUntil ?? Date().addingTimeInterval(3600)
|
|
|
+ let cal = Calendar.current
|
|
|
+ var comps = cal.dateComponents([.year, .month, .day], from: base)
|
|
|
+ let time = cal.dateComponents([.hour, .minute, .second], from: newTimeOnly)
|
|
|
+ comps.hour = time.hour; comps.minute = time.minute; comps.second = time.second
|
|
|
+ cfgStore.value.snoozeUntil = cal.date(from: comps) ?? newTimeOnly
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private func phaseIconName() -> String {
|
|
|
+ let now = Date()
|
|
|
+ let cal = Calendar.current
|
|
|
+ let comps = cal.dateComponents([.year, .month, .day], from: now)
|
|
|
+ func time(_ t: TimeOfDay) -> Date {
|
|
|
+ var c = comps
|
|
|
+ c.hour = t.hour
|
|
|
+ c.minute = t.minute
|
|
|
+ return cal.date(from: c) ?? now
|
|
|
+ }
|
|
|
+ let dayStart = time(cfgStore.value.dayStart)
|
|
|
+ let nightStart = time(cfgStore.value.nightStart)
|
|
|
+
|
|
|
+ let isNight: Bool
|
|
|
+ if dayStart <= nightStart {
|
|
|
+ if now >= nightStart { isNight = true }
|
|
|
+ else if now >= dayStart { isNight = false } else { isNight = true }
|
|
|
+ } else { // crosses midnight
|
|
|
+ if now >= dayStart { isNight = false }
|
|
|
+ else if now >= nightStart { isNight = true } else { isNight = false }
|
|
|
+ }
|
|
|
+ return isNight ? "moon.fill" : "sun.max.fill"
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Sheets
|
|
|
+
|
|
|
+ private func datePickerSheetDate() -> some View {
|
|
|
+ NavigationView {
|
|
|
+ VStack {
|
|
|
+ DatePicker(
|
|
|
+ "Snooze until (date)",
|
|
|
+ selection: snoozeUntilBindingForDate(),
|
|
|
+ displayedComponents: [.date]
|
|
|
+ )
|
|
|
+ .datePickerStyle(.graphical)
|
|
|
+ .padding()
|
|
|
+ Spacer()
|
|
|
+ }
|
|
|
+ .navigationTitle("Snooze Date")
|
|
|
+ .toolbar {
|
|
|
+ ToolbarItem(placement: .confirmationAction) {
|
|
|
+ Button("Done") { showDatePickerDate = false }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .onAppear { cancelAutoHide() }
|
|
|
+ .onDisappear { if !isGlobalSnoozeActive { scheduleAutoHide() } }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func datePickerSheetTime() -> some View {
|
|
|
+ NavigationView {
|
|
|
+ VStack {
|
|
|
+ DatePicker(
|
|
|
+ "Snooze until (time)",
|
|
|
+ selection: snoozeUntilBindingForTime(),
|
|
|
+ displayedComponents: [.hourAndMinute]
|
|
|
+ )
|
|
|
+ .datePickerStyle(.wheel)
|
|
|
+ .labelsHidden()
|
|
|
+ .padding()
|
|
|
+ Spacer()
|
|
|
+ }
|
|
|
+ .navigationTitle("Snooze Time")
|
|
|
+ .toolbar {
|
|
|
+ ToolbarItem(placement: .confirmationAction) {
|
|
|
+ Button("Done") { showDatePickerTime = false }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .onAppear { cancelAutoHide() }
|
|
|
+ .onDisappear { if !isGlobalSnoozeActive { scheduleAutoHide() } }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private extension View {
|