Przeglądaj źródła

Rework device alert views along tester feedback

trioneer 1 tydzień temu
rodzic
commit
019d41a6a3

+ 19 - 2
Trio/Sources/Models/GlucoseAlerts/DeviceAlertSeverity.swift

@@ -35,9 +35,26 @@ enum DeviceAlertSeverity: String, Codable, CaseIterable, Identifiable {
         case .critical:
             return String(localized: "Overrides Silence & Focus Mode. For situations requiring immediate attention.")
         case .timeSensitive:
-            return String(localized: "Pierces banner suppression but obeys Silence & Focus Mode.")
+            return String(localized: "Pierces banner suppression but obeys Silence & Focus Mode by default.")
         case .normal:
-            return String(localized: "Default notification banner. Suppressed by Silence & Focus Mode.")
+            return String(localized: "Default notification banner. Suppressed by Silence & Focus Mode by default.")
+        }
+    }
+
+    var hintText: String {
+        switch self {
+        case .critical:
+            return String(
+                localized: "For situations that require prompt attention. These break through Silent Mode, Do Not Disturb, and any Focus you have enabled. Examples: a pump fault, an occlusion, or Trio not looping for too long. Heads up: if your build of Trio has Apple's Critical Alerts entitlement, iOS plays its own critical alert sound and the sound you picked for this alert category is ignored."
+            )
+        case .timeSensitive:
+            return String(
+                localized: "For things you should know about soon, but not 'act right now'. These can break through banner suppression on the lock screen, but they still obey Silent Mode and Focus by default. Examples: reservoir running low, pod or patch expiring soon, or glucose data going stale."
+            )
+        case .normal:
+            return String(
+                localized: "For everyday heads-up notifications. These behave like a standard banner — they stay quiet when your phone is silenced or a Focus is on. Examples: an algorithm error, a sensor expiration reminder, or a time-zone change being detected."
+            )
         }
     }
 

+ 1 - 1
Trio/Sources/Modules/DeviceAlarms/View/DeviceAlarmEditorView.swift

@@ -60,7 +60,7 @@ struct DeviceAlarmEditorView: View {
                     soundFilename: $working.soundFilename
                 )
 
-                Section(header: Text("Applies To")) {
+                Section(header: Text("Applies To (Cannot be changed)")) {
                     ForEach(conceptsForTier(working.severity), id: \.self) { concept in
                         Text(concept.displayTitle)
                             .font(.footnote)

+ 68 - 34
Trio/Sources/Modules/DeviceAlarms/View/DeviceAlarmsRootView.swift

@@ -4,11 +4,13 @@ import Swinject
 private enum DeviceAlarmSheet: Identifiable {
     case picker
     case editor(DeviceAlertSeverityConfig, isNew: Bool)
+    case help(DeviceAlertSeverity)
 
     var id: String {
         switch self {
         case .picker: return "picker"
         case let .editor(config, _): return config.id.uuidString
+        case let .help(severity): return "helpSheet_" + severity.id
         }
     }
 }
@@ -22,16 +24,43 @@ extension DeviceAlarms {
         @State private var sheet: DeviceAlarmSheet?
         @State private var pendingNewSeverity: DeviceAlertSeverity?
 
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: AnyView?
+
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
         var body: some View {
             List {
                 ForEach(DeviceAlertSeverity.allCases) { severity in
-                    Section(
-                        header: header(for: severity),
-                        footer: footer(for: severity)
-                    ) {
+                    Section {
+                        VStack(alignment: .leading, spacing: 5) {
+                            HStack {
+                                Image(systemName: severityIcon(for: severity))
+                                    .foregroundStyle(severityTint(for: severity))
+                                Text(severity.displayName)
+                                Spacer()
+                            }.font(.headline)
+
+                            HStack(alignment: .center) {
+                                Text(severity.blurb)
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                                    .lineLimit(nil)
+                                    .fixedSize(horizontal: false, vertical: true)
+                                Spacer()
+                                Button(action: {
+                                    sheet = .help(severity)
+                                }) {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                                .buttonStyle(BorderlessButtonStyle())
+                            }.padding(.vertical, 5)
+                        }
+
                         ForEach(store.configs(in: severity)) { config in
                             row(for: config)
                                 .opacity(config.isEnabled ? 1 : 0.5)
@@ -41,6 +70,7 @@ extension DeviceAlarms {
 
                 Section {
                     Text("Day & Night Windows")
+                        .foregroundStyle(Color.accentColor)
                         .navigationLink(to: .alarmWindows, from: self)
                 }.listRowBackground(Color.chart)
             }
@@ -68,6 +98,24 @@ extension DeviceAlarms {
                         onDone: { sheet = nil },
                         onCancel: { sheet = nil }
                     )
+                case let .help(severity):
+                    SettingInputHintView(
+                        hintDetent: $hintDetent,
+                        shouldDisplayHint: Binding(
+                            get: { sheet != nil },
+                            set: { if !$0 { sheet = nil } }
+                        ),
+                        hintLabel: String(
+                            localized: "\(severity.displayName) Device Alerts",
+                            comment: "Device Alerts help sheet label; text reads: '<severity level> Device Alerts'."
+                        ),
+                        hintText: selectedVerboseHint ?? AnyView(
+                            VStack(alignment: .leading, spacing: 10) {
+                                Text(severity.hintText)
+                            }
+                        ),
+                        sheetTitle: String(localized: "Help", comment: "Help sheet title")
+                    )
                 }
             }
             .onAppear(perform: configureView)
@@ -85,20 +133,6 @@ extension DeviceAlarms {
             }
         }
 
-        // MARK: - Section header / footer
-
-        private func header(for severity: DeviceAlertSeverity) -> some View {
-            HStack {
-                Image(systemName: severityIcon(for: severity))
-                    .foregroundStyle(severityTint(for: severity))
-                Text(severity.displayName)
-            }
-        }
-
-        private func footer(for severity: DeviceAlertSeverity) -> some View {
-            Text(severity.blurb)
-        }
-
         // MARK: - Row
 
         @ViewBuilder private func row(for config: DeviceAlertSeverityConfig) -> some View {
@@ -131,22 +165,22 @@ extension DeviceAlarms {
         }
 
         private func soundSummary(for config: DeviceAlertSeverityConfig) -> some View {
-            let icon: String
-            let label: String
-            if !config.playsSound {
-                icon = "speaker.slash.fill"
-                label = String(localized: "Sound off")
-            } else if config.overridesSilenceAndDND {
-                icon = "speaker.wave.3.fill"
-                label =
-                    "\(AlarmSoundCatalog.displayName(for: config.soundFilename)) • \(String(localized: "Overrides Silence & Focus Mode"))"
-            } else {
-                icon = "speaker.fill"
-                label = AlarmSoundCatalog.displayName(for: config.soundFilename)
-            }
-            return HStack(spacing: 4) {
-                Image(systemName: icon)
-                Text(label)
+            // Show the sound and override facts independently. Previously
+            // the override badge was hidden when sound was off, but "sound
+            // off + override on" is a valid combo (silent + haptic that
+            // breaks through Focus / Sleep) and the user needs to see it.
+            HStack(spacing: 6) {
+                HStack(spacing: 4) {
+                    Image(systemName: config.playsSound ? "speaker.wave.2.fill" : "speaker.slash.fill")
+                    Text(config.playsSound ? "Sound on" : "Sound off")
+                }
+                if config.overridesSilenceAndDND {
+                    Text("·")
+                    HStack(spacing: 4) {
+                        Image(systemName: "bell.badge.fill")
+                        Text("Overrides Focus")
+                    }
+                }
             }
             .font(.footnote)
             .foregroundColor(.secondary)

+ 1 - 0
Trio/Sources/Modules/GlucoseAlerts/View/GlucoseAlertsRootView.swift

@@ -64,6 +64,7 @@ extension GlucoseAlerts {
                 // FIXME: make this into a nice setting with mini and verbose hint
                 Section {
                     Text("Day & Night Windows")
+                        .foregroundStyle(Color.accentColor)
                         .navigationLink(to: .alarmWindows, from: self)
                 }.listRowBackground(Color.chart)
 

+ 1 - 1
Trio/Sources/Views/SettingInputSection.swift

@@ -236,7 +236,7 @@ struct SettingInputSection<VerboseHint: View>: View {
         }.padding(.top)
     }
 
-    private func hintSection(
+    public func hintSection(
         miniHint: String,
         shouldDisplayHint: Binding<Bool>,
         verboseHint: VerboseHint,