AlarmListView.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. // LoopFollow
  2. // AlarmListView.swift
  3. // Created by Jonas Björkert on 2025-04-26.
  4. import SwiftUI
  5. struct AddAlarmSheet: View {
  6. let onSelect: (AlarmType) -> Void
  7. @Environment(\.dismiss) private var dismiss
  8. private let columns = [
  9. GridItem(.adaptive(minimum: 110), spacing: 16),
  10. ]
  11. var body: some View {
  12. NavigationStack {
  13. ScrollView {
  14. LazyVGrid(columns: columns, spacing: 16) {
  15. ForEach(AlarmType.Group.allCases, id: \.self) { group in
  16. if AlarmType.allCases.contains(where: { $0.group == group }) {
  17. Section(header: Text(group.rawValue)
  18. .font(.headline)
  19. .frame(maxWidth: .infinity, alignment: .leading)
  20. .padding(.horizontal, 4)
  21. ) {
  22. ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in
  23. AlarmTile(type: type) {
  24. onSelect(type)
  25. }
  26. }
  27. }
  28. }
  29. }
  30. }
  31. .padding()
  32. }
  33. .navigationTitle("Add Alarm")
  34. .toolbar {
  35. ToolbarItem(placement: .cancellationAction) {
  36. Button("Cancel") { dismiss() }
  37. }
  38. }
  39. }
  40. .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
  41. }
  42. }
  43. private struct AlarmTile: View {
  44. let type: AlarmType
  45. let action: () -> Void
  46. var body: some View {
  47. Button(action: action) {
  48. VStack(spacing: 8) {
  49. Image(systemName: type.icon)
  50. .font(.title2)
  51. .foregroundColor(.accentColor)
  52. Text(type.rawValue)
  53. .font(.subheadline)
  54. .multilineTextAlignment(.center)
  55. .lineLimit(2)
  56. Text(type.blurb)
  57. .font(.caption2)
  58. .foregroundColor(.secondary)
  59. .multilineTextAlignment(.center)
  60. .lineLimit(2)
  61. }
  62. .padding()
  63. .frame(maxWidth: .infinity, minHeight: 110)
  64. .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
  65. }
  66. .buttonStyle(.plain)
  67. }
  68. }
  69. private enum SheetInfo: Identifiable {
  70. case picker
  71. case editor(id: UUID, isNew: Bool)
  72. var id: UUID {
  73. switch self {
  74. case .picker:
  75. return UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
  76. case let .editor(id, _):
  77. return id
  78. }
  79. }
  80. }
  81. struct AlarmListView: View {
  82. @ObservedObject private var store = Storage.shared.alarms
  83. @State private var sheetInfo: SheetInfo?
  84. @State private var deleteAfterDismiss: UUID?
  85. @State private var selectedAlarm: Alarm?
  86. var body: some View {
  87. List {
  88. ForEach(store.value) { alarm in
  89. Button {
  90. selectedAlarm = alarm
  91. sheetInfo = .editor(id: alarm.id, isNew: false)
  92. } label: {
  93. HStack(spacing: 12) {
  94. Glyph(
  95. symbol: alarm.type.icon,
  96. tint: alarm.isEnabled ? .white : Color(uiColor: .darkGray)
  97. )
  98. .overlay {
  99. if let until = alarm.snoozedUntil, until > Date() {
  100. Image(systemName: "zzz")
  101. .font(.caption.bold())
  102. .foregroundColor(.secondary)
  103. .shadow(color: .black, radius: 2)
  104. .offset(x: 8, y: 8)
  105. }
  106. }
  107. Text(alarm.name)
  108. .frame(maxWidth: .infinity, alignment: .leading)
  109. .foregroundColor(.primary)
  110. }
  111. }
  112. }
  113. .onDelete { store.value.remove(atOffsets: $0) }
  114. }
  115. .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in
  116. sheetContent(for: info)
  117. }
  118. .navigationBarTitle("Alarms", displayMode: .inline)
  119. .toolbar {
  120. ToolbarItem(placement: .primaryAction) {
  121. Button { sheetInfo = .picker } label: { Image(systemName: "plus") }
  122. }
  123. }
  124. .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil)
  125. }
  126. private func handleSheetDismiss() {
  127. if let id = deleteAfterDismiss,
  128. let idx = store.value.firstIndex(where: { $0.id == id })
  129. {
  130. store.value.remove(at: idx)
  131. }
  132. deleteAfterDismiss = nil
  133. }
  134. @ViewBuilder
  135. private func sheetContent(for info: SheetInfo) -> some View {
  136. switch info {
  137. case .picker:
  138. AddAlarmSheet { type in
  139. let new = Alarm(type: type)
  140. store.value.append(new)
  141. sheetInfo = .editor(id: new.id, isNew: true)
  142. }
  143. case let .editor(id, isNew):
  144. if let idx = store.value.firstIndex(where: { $0.id == id }) {
  145. AlarmEditor(
  146. alarm: $store.value[idx],
  147. isNew: isNew,
  148. onDone: { sheetInfo = nil },
  149. onCancel: {
  150. if isNew { deleteAfterDismiss = id }
  151. sheetInfo = nil
  152. }
  153. )
  154. } else {
  155. Text("Alarm not found").padding()
  156. }
  157. }
  158. }
  159. private func iconOpacity(for alarm: Alarm) -> Double {
  160. if !alarm.isEnabled { return 0.35 }
  161. if let until = alarm.snoozedUntil, until > Date() { return 0.55 }
  162. return 1.0
  163. }
  164. }