Просмотр исходного кода

Gate Add Alarm picker + editor window options on already-configured alarms

trioneer недель назад: 2
Родитель
Сommit
a224748ba2

+ 16 - 4
Trio/Sources/Modules/GlucoseAlerts/View/AddGlucoseAlertSheet.swift

@@ -1,6 +1,7 @@
 import SwiftUI
 
 struct AddGlucoseAlertSheet: View {
+    @ObservedObject var store: GlucoseAlertsStore
     let onPick: (GlucoseAlertType) -> Void
 
     @Environment(\.dismiss) private var dismiss
@@ -12,7 +13,10 @@ struct AddGlucoseAlertSheet: View {
             List {
                 Section {
                     ForEach(GlucoseAlertType.allCases) { type in
+                        let available = store.availableActiveOptions(forNewAlarmOfType: type)
+                        let isAvailable = !available.isEmpty
                         Button {
+                            guard isAvailable else { return }
                             onPick(type)
                             dismiss()
                         } label: {
@@ -20,16 +24,20 @@ struct AddGlucoseAlertSheet: View {
                                 VStack(alignment: .leading, spacing: 2) {
                                     Text(type.displayName)
                                         .foregroundColor(.primary)
-                                    Text(type.blurb)
+                                    Text(isAvailable ? type.blurb : unavailableHint(forType: type))
                                         .font(.footnote)
                                         .foregroundColor(.secondary)
                                 }
                                 Spacer()
-                                Image(systemName: "chevron.right")
-                                    .font(.footnote)
-                                    .foregroundColor(.secondary)
+                                if isAvailable {
+                                    Image(systemName: "chevron.right")
+                                        .font(.footnote)
+                                        .foregroundColor(.secondary)
+                                }
                             }
                         }
+                        .disabled(!isAvailable)
+                        .opacity(isAvailable ? 1 : 0.5)
                     }
                 }.listRowBackground(Color.chart)
             }
@@ -43,4 +51,8 @@ struct AddGlucoseAlertSheet: View {
             }
         }
     }
+
+    private func unavailableHint(forType type: GlucoseAlertType) -> String {
+        String(localized: "Already set for Day & Night. Delete one of these to add a new one.")
+    }
 }

+ 8 - 1
Trio/Sources/Modules/GlucoseAlerts/View/Components/AlarmActiveSection.swift

@@ -2,6 +2,9 @@ import SwiftUI
 
 struct AlarmActiveSection: View {
     @Binding var activeOption: ActiveOption
+    /// Subset of `ActiveOption` cases the picker is allowed to offer.
+    /// Defaults to all cases when omitted.
+    var allowed: [ActiveOption] = ActiveOption.allCases
 
     var body: some View {
         Section(
@@ -10,7 +13,11 @@ struct AlarmActiveSection: View {
                 "Day and Night windows are configured globally on the Alarms screen."
             )
         ) {
-            AlarmEnumMenuPicker(title: String(localized: "Active"), selection: $activeOption)
+            AlarmEnumMenuPicker(
+                title: String(localized: "Active"),
+                selection: $activeOption,
+                allowed: allowed
+            )
         }.listRowBackground(Color.chart)
     }
 }

+ 15 - 1
Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertEditorView.swift

@@ -4,6 +4,17 @@ struct GlucoseAlertEditorView: View {
     @ObservedObject var store: GlucoseAlertsStore
     let alertID: UUID
     let isNew: Bool
+
+    /// Windows the user can still pick without overlapping another alarm of
+    /// the same type. The being-edited alarm is excluded from "taken" so its
+    /// current option stays valid.
+    private var allowedActiveOptions: [ActiveOption] {
+        let available = store.availableActiveOptions(
+            forType: working.type,
+            excludingAlertID: isNew ? nil : alertID
+        )
+        return ActiveOption.allCases.filter { available.contains($0) }
+    }
     let units: GlucoseUnits
     var onDone: () -> Void
     var onCancel: () -> Void
@@ -43,7 +54,10 @@ struct GlucoseAlertEditorView: View {
                 case .carbsRequired: carbsRequiredBody
                 }
 
-                AlarmActiveSection(activeOption: $working.activeOption)
+                AlarmActiveSection(
+                    activeOption: $working.activeOption,
+                    allowed: allowedActiveOptions
+                )
                 AlarmAudioSection(
                     playsSound: $working.playsSound,
                     soundFilename: $working.soundFilename

+ 9 - 2
Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertsRootView.swift

@@ -104,7 +104,7 @@ extension GlucoseAlerts {
             .sheet(item: $sheet, onDismiss: handleSheetDismiss) { which in
                 switch which {
                 case .picker:
-                    AddGlucoseAlertSheet { type in
+                    AddGlucoseAlertSheet(store: store) { type in
                         pendingNewType = type
                         sheet = nil
                     }
@@ -135,7 +135,14 @@ extension GlucoseAlerts {
             guard let type = pendingNewType else { return }
             pendingNewType = nil
             DispatchQueue.main.async {
-                sheet = .editor(GlucoseAlert(type: type), isNew: true)
+                var seed = GlucoseAlert(type: type)
+                // Default new alarm to the first available window so it
+                // doesn't overlap with whatever is already configured.
+                let available = store.availableActiveOptions(forNewAlarmOfType: type)
+                if let first = ActiveOption.allCases.first(where: available.contains) {
+                    seed.activeOption = first
+                }
+                sheet = .editor(seed, isNew: true)
             }
         }
 

+ 26 - 0
Trio/Sources/Services/Alerts/GlucoseAlertsStore.swift

@@ -95,6 +95,32 @@ final class GlucoseAlertsStore: ObservableObject {
         alerts.filter { $0.type == alert.type }.count > 1
     }
 
+    /// `ActiveOption`s a new alarm of `type` could still occupy without
+    /// overlapping an existing alarm of the same type. `.always` covers both
+    /// windows, so it's removed as soon as either `.day` or `.night` is taken.
+    /// Returns the empty set when the type is fully covered (either `.always`
+    /// is already present, or both `.day` AND `.night` are present).
+    func availableActiveOptions(forNewAlarmOfType type: GlucoseAlertType) -> Set<ActiveOption> {
+        availableActiveOptions(forType: type, excludingAlertID: nil)
+    }
+
+    /// Variant used when editing an existing alarm — excludes the alarm being
+    /// edited from the "taken" set so its current window stays valid.
+    func availableActiveOptions(
+        forType type: GlucoseAlertType,
+        excludingAlertID excludedID: UUID?
+    ) -> Set<ActiveOption> {
+        let taken = Set(
+            alerts
+                .filter { $0.type == type && $0.id != excludedID }
+                .map(\.activeOption)
+        )
+        var available = Set(ActiveOption.allCases)
+        available.subtract(taken)
+        if !taken.isEmpty { available.remove(.always) }
+        return available
+    }
+
     // MARK: - Codable helpers
 
     private static func decode<T: Decodable>(