Explorar el Código

Alarm selection

Jonas Björkert hace 1 año
padre
commit
ef140b41e3
Se han modificado 1 ficheros con 168 adiciones y 38 borrados
  1. 168 38
      LoopFollow/Alarm/AlarmListView.swift

+ 168 - 38
LoopFollow/Alarm/AlarmListView.swift

@@ -6,74 +6,204 @@
 //  Copyright © 2025 Jon Fawcett. All rights reserved.
 //
 
-
 import SwiftUI
 
-/// Displays all configured alarms and allows adding a new one by selecting its type.
+extension AlarmType {
+    enum Group: String, CaseIterable {
+        case glucose = "Glucose"
+        case insulin = "Insulin / Food"
+        case device  = "Device / System"
+        case other   = "Other"
+    }
+
+    var group: Group {
+        switch self {
+        case .low, .high, .fastDrop, .fastRise, .missedReading:
+            return .glucose
+        case .iob, .bolus, .cob, .missedBolus, .recBolus:
+            return .insulin
+        case .battery, .batteryDrop, .pump, .pumpChange,
+                .sensorChange, .notLooping, .buildExpire:
+            return .device
+        default:
+            return .other
+        }
+    }
+
+    var icon: String {
+        switch self {
+        case .low       : return "arrow.down"
+        case .high      : return "arrow.up"
+        case .fastDrop  : return "arrow.down.to.line"
+        case .fastRise  : return "arrow.up.to.line"
+        case .missedReading: return "wifi.slash"
+
+        case .iob, .bolus: return "syringe"
+        case .cob       : return "fork.knife"
+        case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath"
+        case .recBolus  : return "bolt.horizontal"
+
+        case .battery   : return "battery.25"
+        case .batteryDrop: return "battery.100.bolt"
+        case .pump      : return "drop"
+        case .pumpChange: return "arrow.triangle.2.circlepath"
+        case .sensorChange: return "sensor.tag.radiowaves.forward"
+
+        case .notLooping: return "circle.slash"
+        case .buildExpire: return "calendar.badge.exclamationmark"
+
+        case .overrideStart: return "play.circle"
+        case .overrideEnd  : return "stop.circle"
+        case .tempTargetStart: return "flag"
+        case .tempTargetEnd  : return "flag.slash"
+        }
+    }
+
+    var blurb: String {
+        switch self {
+        case .low:            return "Alerts when BG goes below a limit."
+        case .high:           return "Alerts when BG rises above a limit."
+        case .fastDrop:       return "Rapid downward BG trend."
+        case .fastRise:       return "Rapid upward BG trend."
+        case .missedReading:  return "No CGM data for X minutes."
+
+        case .iob:            return "High insulin-on-board."
+        case .bolus:          return "Large individual bolus."
+        case .cob:            return "High carbs-on-board."
+        case .missedBolus:    return "Carbs without bolus."
+        case .recBolus:       return "Recommended bolus issued."
+
+        case .battery:        return "Pump / phone battery low."
+        case .batteryDrop:    return "Battery drops quickly."
+        case .pump:           return "Reservoir level low."
+        case .pumpChange:     return "Infusion-set change due."
+        case .sensorChange:   return "Sensor change due."
+        case .notLooping:     return "Loop hasn’t completed."
+        case .buildExpire:    return "Follow-app build expiring."
+
+        case .overrideStart:  return "Override just started."
+        case .overrideEnd:    return "Override ended."
+        case .tempTargetStart:return "Temp target started."
+        case .tempTargetEnd:  return "Temp target ended."
+        }
+    }
+}
+
+struct AddAlarmSheet: View {
+    let onSelect: (AlarmType) -> Void
+    @Environment(\.dismiss) private var dismiss
+
+    private let columns = [
+        GridItem(.adaptive(minimum: 110), spacing: 16)
+    ]
+
+    var body: some View {
+        NavigationStack {
+            ScrollView {
+                LazyVGrid(columns: columns, spacing: 16) {
+                    ForEach(AlarmType.Group.allCases, id: \.self) { group in
+                        if AlarmType.allCases.contains(where: { $0.group == group }) {
+                            Section(header:
+                                        Text(group.rawValue)
+                                .font(.headline)
+                                .frame(maxWidth: .infinity, alignment: .leading)
+                                .padding(.horizontal, 4)
+                            ) {
+                                ForEach(AlarmType.allCases.filter { $0.group == group },
+                                        id: \.self) { type in
+                                    AlarmTile(type: type) {
+                                        onSelect(type)
+                                        dismiss()
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                .padding()
+            }
+            .navigationTitle("Add Alarm")
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("Cancel") { dismiss() }
+                }
+            }
+        }
+    }
+}
+
+private struct AlarmTile: View {
+    let type: AlarmType
+    let action: () -> Void
+
+    var body: some View {
+        Button(action: action) {
+            VStack(spacing: 8) {
+                Image(systemName: type.icon)
+                    .font(.title2)
+                    .foregroundColor(.accentColor)
+                Text(type.rawValue)
+                    .font(.subheadline)
+                    .multilineTextAlignment(.center)
+                    .lineLimit(2)
+                Text(type.blurb)
+                    .font(.caption2)
+                    .foregroundColor(.secondary)
+                    .multilineTextAlignment(.center)
+                    .lineLimit(2)
+            }
+            .padding()
+            .frame(maxWidth: .infinity, minHeight: 110)
+            .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12))
+        }
+        .buttonStyle(.plain)
+    }
+}
+
 struct AlarmListView: View {
     @ObservedObject private var store = Storage.shared.alarms
-    @Environment(\.presentationMode) var presentationMode
-    @State private var showingTypePicker = false
+    @Environment(\.dismiss) private var dismiss
+
+    @State private var showAddSheet = false
     @State private var editingAlarmID: UUID?
 
     var body: some View {
-        NavigationView {
+        NavigationStack {
             List {
-                // TODO: sort these in the alarm prio order, as they are evaluated
                 ForEach(store.value) { alarm in
                     NavigationLink(alarm.name) {
                         AlarmEditor(alarm: binding(for: alarm))
                     }
                 }
-                .onDelete { idxs in
-                    store.value.remove(atOffsets: idxs)
-                }
+                .onDelete { store.value.remove(atOffsets: $0) }
             }
             .navigationTitle("Alarms")
             .toolbar {
-                ToolbarItem(placement: .navigationBarTrailing) {
-                    Button {
-                        showingTypePicker = true
-                    } label: {
-                        Image(systemName: "plus")
-                    }
-                }
                 ToolbarItem(placement: .navigationBarLeading) {
-                    Button("Done") {
-                        presentationMode.wrappedValue.dismiss()
-                    }
+                    Button("Done") { dismiss() }
+                }
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    Button { showAddSheet = true } label: { Image(systemName: "plus") }
                 }
             }
-            // Step 1: pick a type for the new alarm
-            // TODO: Sort these in the type order
-            .actionSheet(isPresented: $showingTypePicker) {
-                ActionSheet(
-                    title: Text("Select Alarm Type"),
-                    buttons: AlarmType.allCases.map { type in
-                        .default(Text(type.rawValue)) {
-                            let newAlarm = Alarm(type: type)
-                            store.value.append(newAlarm)
-                            editingAlarmID = newAlarm.id
-                        }
-                    } + [.cancel()]
-                )
+            .sheet(isPresented: $showAddSheet) {
+                AddAlarmSheet { type in
+                    let new = Alarm(type: type)
+                    store.value.append(new)
+                    editingAlarmID = new.id
+                }
             }
-            // Step 2: when an ID is set, present the editor
             .sheet(item: $editingAlarmID) { id in
                 if let idx = store.value.firstIndex(where: { $0.id == id }) {
                     AlarmEditor(alarm: $store.value[idx])
-                } else {
-                    Text("Alarm not found")
-                        .padding()
                 }
             }
         }
     }
 
-    /// Find and return a binding to the given alarm in the store
     private func binding(for alarm: Alarm) -> Binding<Alarm> {
         guard let idx = store.value.firstIndex(where: { $0.id == alarm.id }) else {
-            fatalError("Alarm not found in store")
+            fatalError("Alarm not found")
         }
         return $store.value[idx]
     }