AlarmListView.swift 5.4 KB

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