AlarmListView.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. // LoopFollow
  2. // AlarmListView.swift
  3. // Created by Jonas Björkert.
  4. import SwiftUI
  5. private enum SheetInfo: Identifiable {
  6. case picker
  7. case editor(id: UUID, isNew: Bool)
  8. var id: UUID {
  9. switch self {
  10. case .picker:
  11. return UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
  12. case let .editor(id, _):
  13. return id
  14. }
  15. }
  16. }
  17. struct AlarmListView: View {
  18. @ObservedObject private var store = Storage.shared.alarms
  19. @State private var sheetInfo: SheetInfo?
  20. @State private var deleteAfterDismiss: UUID?
  21. @State private var selectedAlarm: Alarm?
  22. // MARK: - Categorized Alarms
  23. private var snoozedAlarms: [Alarm] {
  24. store.value.filter { $0.snoozedUntil ?? .distantPast > Date() && $0.isEnabled }
  25. .sorted(by: Alarm.byPriorityThenSpec)
  26. }
  27. private var activeAlarms: [Alarm] {
  28. store.value.filter { $0.isEnabled && ($0.snoozedUntil ?? .distantPast <= Date()) }
  29. .sorted(by: Alarm.byPriorityThenSpec)
  30. }
  31. private var inactiveAlarms: [Alarm] {
  32. store.value.filter { !$0.isEnabled }
  33. .sorted(by: Alarm.byPriorityThenSpec)
  34. }
  35. // MARK: - Formatters
  36. private var timeFormatter: DateFormatter {
  37. let formatter = DateFormatter()
  38. formatter.dateStyle = .none
  39. formatter.timeStyle = .short
  40. return formatter
  41. }
  42. // MARK: - Body
  43. var body: some View {
  44. List {
  45. // --- SNOOZED ALARMS SECTION ---
  46. if !snoozedAlarms.isEmpty {
  47. Section(header: Text("Snoozed")) {
  48. ForEach(snoozedAlarms) { alarm in
  49. alarmRow(for: alarm)
  50. }
  51. }
  52. }
  53. // --- ACTIVE ALARMS SECTION ---
  54. if !activeAlarms.isEmpty {
  55. Section(header: Text("Active")) {
  56. ForEach(activeAlarms) { alarm in
  57. alarmRow(for: alarm)
  58. }
  59. }
  60. }
  61. // --- INACTIVE ALARMS SECTION ---
  62. if !inactiveAlarms.isEmpty {
  63. Section(header: Text("Inactive")) {
  64. ForEach(inactiveAlarms) { alarm in
  65. alarmRow(for: alarm)
  66. .opacity(0.6)
  67. }
  68. }
  69. }
  70. }
  71. .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in
  72. sheetContent(for: info)
  73. }
  74. .navigationBarTitle("Alarms", displayMode: .inline)
  75. .toolbar {
  76. ToolbarItem(placement: .primaryAction) {
  77. Button { sheetInfo = .picker } label: { Image(systemName: "plus") }
  78. }
  79. }
  80. .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
  81. }
  82. // MARK: - Views
  83. @ViewBuilder
  84. private func alarmRow(for alarm: Alarm) -> some View {
  85. Button {
  86. selectedAlarm = alarm
  87. sheetInfo = .editor(id: alarm.id, isNew: false)
  88. } label: {
  89. HStack(spacing: 12) {
  90. Glyph(
  91. symbol: alarm.type.icon,
  92. tint: .primary
  93. )
  94. VStack(alignment: .leading, spacing: 2) {
  95. Text(alarm.name)
  96. .foregroundColor(.primary)
  97. if let until = alarm.snoozedUntil, until > Date() {
  98. HStack(spacing: 4) {
  99. Image(systemName: "zzz")
  100. .font(.caption2)
  101. Text("Snoozed until \(until, formatter: timeFormatter)")
  102. .font(.caption)
  103. }
  104. .foregroundColor(.secondary)
  105. }
  106. }
  107. Spacer()
  108. Image(systemName: "chevron.right")
  109. .font(.footnote.weight(.semibold))
  110. .foregroundColor(.secondary)
  111. }
  112. }
  113. .swipeActions {
  114. Button(role: .destructive) {
  115. store.value.removeAll { $0.id == alarm.id }
  116. } label: {
  117. Label("Delete", systemImage: "trash.fill")
  118. }
  119. }
  120. }
  121. // MARK: - Sheet Management
  122. private func handleSheetDismiss() {
  123. if let id = deleteAfterDismiss,
  124. let idx = store.value.firstIndex(where: { $0.id == id })
  125. {
  126. store.value.remove(at: idx)
  127. }
  128. deleteAfterDismiss = nil
  129. }
  130. @ViewBuilder
  131. private func sheetContent(for info: SheetInfo) -> some View {
  132. switch info {
  133. case .picker:
  134. AddAlarmSheet { type in
  135. let new = Alarm(type: type)
  136. store.value.append(new)
  137. sheetInfo = .editor(id: new.id, isNew: true)
  138. }
  139. case let .editor(id, isNew):
  140. if let idx = store.value.firstIndex(where: { $0.id == id }) {
  141. AlarmEditor(
  142. alarm: $store.value[idx],
  143. isNew: isNew,
  144. onDone: { sheetInfo = nil },
  145. onCancel: {
  146. if isNew { deleteAfterDismiss = id }
  147. sheetInfo = nil
  148. },
  149. onDelete: {
  150. deleteAfterDismiss = id
  151. sheetInfo = nil
  152. }
  153. )
  154. } else {
  155. Text("Alarm not found").padding()
  156. }
  157. }
  158. }
  159. }