AlarmSelectionView.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // LoopFollow
  2. // AlarmSelectionView.swift
  3. import SwiftUI
  4. struct AlarmSelectionView: View {
  5. @ObservedObject private var allAlarms = Storage.shared.alarms
  6. @State private var selectedAlarmIds: Set<UUID> = []
  7. @State private var showingCharacterLimitAlert = false
  8. let exportedAlarmIds: Set<UUID>
  9. let onConfirm: ([Alarm]) -> Void
  10. let onCancel: () -> Void
  11. // QR Code character limits (calibrated for alarm exports)
  12. private let maxQRCharacters = 2000
  13. private let maxRecommendedAlarms = 5
  14. // Computed property for actual character count (used internally)
  15. private var actualCharacterCount: Int {
  16. let selectedAlarms = allAlarms.value.filter { selectedAlarmIds.contains($0.id) }
  17. let testExport = AlarmSettingsExport(
  18. version: AppVersionManager().version(),
  19. alarms: selectedAlarms,
  20. alarmConfiguration: Storage.shared.alarmConfiguration.value
  21. )
  22. if let jsonString = testExport.encodeToJSON() {
  23. return jsonString.count
  24. }
  25. return 0
  26. }
  27. private var exceedsCharacterLimit: Bool {
  28. return actualCharacterCount > maxQRCharacters
  29. }
  30. var body: some View {
  31. NavigationView {
  32. VStack {
  33. // Character count indicator
  34. characterCountView
  35. List {
  36. ForEach(allAlarms.value) { alarm in
  37. AlarmSelectionRow(
  38. alarm: alarm,
  39. isSelected: selectedAlarmIds.contains(alarm.id),
  40. isExported: exportedAlarmIds.contains(alarm.id),
  41. isDisabled: !canSelectAlarm(alarm),
  42. onToggle: { toggleAlarm(alarm) }
  43. )
  44. }
  45. }
  46. }
  47. .navigationTitle("Select Alarms to Export")
  48. .navigationBarTitleDisplayMode(.inline)
  49. .navigationBarItems(
  50. leading: Button("Cancel") {
  51. onCancel()
  52. },
  53. trailing: Button("Export") {
  54. let selectedAlarms = allAlarms.value.filter { selectedAlarmIds.contains($0.id) }
  55. onConfirm(selectedAlarms)
  56. }
  57. .disabled(selectedAlarmIds.isEmpty)
  58. )
  59. }
  60. .alert("Character Limit Reached", isPresented: $showingCharacterLimitAlert) {
  61. Button("OK") {}
  62. } message: {
  63. Text("Adding this alarm would exceed the QR code character limit. Please remove some alarms first.")
  64. }
  65. }
  66. private var characterCountView: some View {
  67. VStack(spacing: 8) {
  68. HStack {
  69. Text("Selected Alarms: \(selectedAlarmIds.count)")
  70. .font(.headline)
  71. Spacer()
  72. Text("Max \(maxRecommendedAlarms) alarms at a time")
  73. .font(.caption)
  74. .foregroundColor(.secondary)
  75. }
  76. if selectedAlarmIds.count > 0 {
  77. ProgressView(value: Double(selectedAlarmIds.count), total: Double(maxRecommendedAlarms))
  78. .progressViewStyle(LinearProgressViewStyle(tint: progressBarColor))
  79. }
  80. }
  81. .padding()
  82. .background(Color(.systemGray6))
  83. }
  84. private var progressBarColor: Color {
  85. if exceedsCharacterLimit {
  86. return .red
  87. } else if selectedAlarmIds.count >= maxRecommendedAlarms {
  88. return .blue
  89. } else {
  90. return .green
  91. }
  92. }
  93. private func canSelectAlarm(_ alarm: Alarm) -> Bool {
  94. let testSelection = selectedAlarmIds.union([alarm.id])
  95. let testAlarms = allAlarms.value.filter { testSelection.contains($0.id) }
  96. // Block if we're at or over the recommended alarm limit
  97. if testAlarms.count > maxRecommendedAlarms {
  98. return false
  99. }
  100. // Test actual character count to prevent exceeding QR code limit
  101. let testExport = AlarmSettingsExport(
  102. version: AppVersionManager().version(),
  103. alarms: testAlarms,
  104. alarmConfiguration: Storage.shared.alarmConfiguration.value
  105. )
  106. if let jsonString = testExport.encodeToJSON() {
  107. return jsonString.count <= maxQRCharacters
  108. }
  109. return false
  110. }
  111. private func toggleAlarm(_ alarm: Alarm) {
  112. if selectedAlarmIds.contains(alarm.id) {
  113. selectedAlarmIds.remove(alarm.id)
  114. } else {
  115. if canSelectAlarm(alarm) {
  116. selectedAlarmIds.insert(alarm.id)
  117. } else {
  118. showingCharacterLimitAlert = true
  119. }
  120. }
  121. }
  122. }
  123. struct AlarmSelectionRow: View {
  124. let alarm: Alarm
  125. let isSelected: Bool
  126. let isExported: Bool
  127. let isDisabled: Bool
  128. let onToggle: () -> Void
  129. var body: some View {
  130. Button(action: onToggle) {
  131. HStack {
  132. VStack(alignment: .leading, spacing: 4) {
  133. HStack {
  134. Text(alarm.name)
  135. .font(.headline)
  136. .foregroundColor(isDisabled ? .secondary : .primary)
  137. if isExported {
  138. Image(systemName: "checkmark.circle.fill")
  139. .foregroundColor(.green)
  140. .font(.caption)
  141. }
  142. }
  143. Text(alarmTypeDescription)
  144. .font(.caption)
  145. .foregroundColor(.secondary)
  146. if let aboveBG = alarm.aboveBG {
  147. Text("Above: \(String(format: "%.0f", aboveBG))")
  148. .font(.caption2)
  149. .foregroundColor(.secondary)
  150. }
  151. if let belowBG = alarm.belowBG {
  152. Text("Below: \(String(format: "%.0f", belowBG))")
  153. .font(.caption2)
  154. .foregroundColor(.secondary)
  155. }
  156. }
  157. Spacer()
  158. Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
  159. .foregroundColor(isSelected ? .blue : (isDisabled ? .secondary : .primary))
  160. .font(.title2)
  161. }
  162. .padding(.vertical, 4)
  163. }
  164. .buttonStyle(.plain)
  165. .disabled(isDisabled && !isSelected)
  166. }
  167. private var alarmTypeDescription: String {
  168. switch alarm.type {
  169. case .temporary:
  170. return "Temporary Alert"
  171. case .iob:
  172. return "IOB Alert"
  173. case .cob:
  174. return "COB Alert"
  175. case .low:
  176. return "Low BG Alert"
  177. case .high:
  178. return "High BG Alert"
  179. case .fastDrop:
  180. return "Fast Drop Alert"
  181. case .fastRise:
  182. return "Fast Rise Alert"
  183. case .missedReading:
  184. return "Missed Reading Alert"
  185. case .notLooping:
  186. return "Not Looping Alert"
  187. case .missedBolus:
  188. return "Missed Bolus Alert"
  189. case .futureCarbs:
  190. return "Future Carbs Alert"
  191. case .sensorChange:
  192. return "Sensor Change Alert"
  193. case .pumpChange:
  194. return "Pump Change Alert"
  195. case .pump:
  196. return "Pump Insulin Alert"
  197. case .pumpBattery:
  198. return "Pump Battery Alert"
  199. case .battery:
  200. return "Low Battery"
  201. case .batteryDrop:
  202. return "Battery Drop"
  203. case .recBolus:
  204. return "Rec. Bolus"
  205. case .overrideStart:
  206. return "Override Started"
  207. case .overrideEnd:
  208. return "Override Ended"
  209. case .tempTargetStart:
  210. return "Temp Target Started"
  211. case .tempTargetEnd:
  212. return "Temp Target Ended"
  213. case .buildExpire:
  214. return "Looping app expiration"
  215. case .dbSize:
  216. return "Nightscout Database Size"
  217. }
  218. }
  219. }
  220. #Preview {
  221. AlarmSelectionView(
  222. exportedAlarmIds: [],
  223. onConfirm: { _ in },
  224. onCancel: {}
  225. )
  226. }