|
|
@@ -9,100 +9,44 @@ struct SettingsMenuView: View {
|
|
|
|
|
|
var body: some View {
|
|
|
List {
|
|
|
- dataSection
|
|
|
-
|
|
|
- Section("Display Settings") {
|
|
|
- NavigationRow(title: "General",
|
|
|
- icon: "gearshape",
|
|
|
- value: SettingsRoute.general)
|
|
|
- NavigationRow(title: "Graph",
|
|
|
- icon: "chart.xyaxis.line",
|
|
|
- value: SettingsRoute.graph)
|
|
|
-
|
|
|
- if !nightscoutURL.value.isEmpty {
|
|
|
- NavigationRow(title: "Information Display",
|
|
|
- icon: "info.circle",
|
|
|
- value: SettingsRoute.infoDisplay)
|
|
|
- }
|
|
|
-
|
|
|
- NavigationRow(title: "Units and Metrics",
|
|
|
- icon: "scalemass",
|
|
|
- value: SettingsRoute.units)
|
|
|
-
|
|
|
- NavigationRow(title: "Tabs",
|
|
|
- icon: "rectangle.3.group",
|
|
|
- value: SettingsRoute.tabSettings)
|
|
|
- }
|
|
|
-
|
|
|
- Section("App Settings") {
|
|
|
- NavigationRow(title: "Background Refresh",
|
|
|
- icon: "arrow.clockwise",
|
|
|
- value: SettingsRoute.backgroundRefresh)
|
|
|
-
|
|
|
- NavigationRow(title: "Import/Export",
|
|
|
- icon: "square.and.arrow.down",
|
|
|
- value: SettingsRoute.importExport)
|
|
|
-
|
|
|
- NavigationRow(title: "APN",
|
|
|
- icon: "bell.and.waves.left.and.right",
|
|
|
- value: SettingsRoute.apn)
|
|
|
-
|
|
|
- #if !targetEnvironment(macCatalyst)
|
|
|
- NavigationRow(title: "Live Activity",
|
|
|
- icon: "dot.radiowaves.left.and.right",
|
|
|
- value: SettingsRoute.liveActivity)
|
|
|
- #endif
|
|
|
-
|
|
|
- if !nightscoutURL.value.isEmpty {
|
|
|
- NavigationRow(title: "Remote",
|
|
|
- icon: "antenna.radiowaves.left.and.right",
|
|
|
- value: SettingsRoute.remote)
|
|
|
+ ForEach(SettingsRoute.menuSections(nightscoutConfigured: !nightscoutURL.value.isEmpty), id: \.0) { section, routes in
|
|
|
+ Section(section.rawValue) {
|
|
|
+ ForEach(routes) { route in
|
|
|
+ NavigationRow(title: route.title,
|
|
|
+ icon: route.icon,
|
|
|
+ value: route)
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- Section("Alarms") {
|
|
|
- NavigationRow(title: "Alarms",
|
|
|
- icon: "bell.badge",
|
|
|
- value: SettingsRoute.alarmSettings)
|
|
|
- }
|
|
|
-
|
|
|
- Section("Integrations") {
|
|
|
- NavigationRow(title: "Calendar",
|
|
|
- icon: "calendar",
|
|
|
- value: SettingsRoute.calendar)
|
|
|
-
|
|
|
- NavigationRow(title: "Contact",
|
|
|
- icon: "person.circle",
|
|
|
- value: SettingsRoute.contact)
|
|
|
- }
|
|
|
-
|
|
|
- Section("Advanced Settings") {
|
|
|
- NavigationRow(title: "Advanced",
|
|
|
- icon: "exclamationmark.shield",
|
|
|
- value: SettingsRoute.advanced)
|
|
|
- }
|
|
|
}
|
|
|
.navigationTitle("Settings")
|
|
|
.navigationBarTitleDisplayMode(.large)
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- // MARK: – Section builders
|
|
|
+// MARK: – Sheet routing
|
|
|
|
|
|
- @ViewBuilder
|
|
|
- private var dataSection: some View {
|
|
|
- Section("Data Settings") {
|
|
|
- NavigationRow(title: "Nightscout",
|
|
|
- icon: "network",
|
|
|
- value: SettingsRoute.nightscout)
|
|
|
-
|
|
|
- NavigationRow(title: "Dexcom",
|
|
|
- icon: "sensor.tag.radiowaves.forward",
|
|
|
- value: SettingsRoute.dexcom)
|
|
|
- }
|
|
|
- }
|
|
|
+enum SettingsSection: String, CaseIterable, Hashable {
|
|
|
+ case data = "Data Settings"
|
|
|
+ case display = "Display Settings"
|
|
|
+ case app = "App Settings"
|
|
|
+ case alarms = "Alarms"
|
|
|
+ case integrations = "Integrations"
|
|
|
+ case advanced = "Advanced Settings"
|
|
|
}
|
|
|
|
|
|
-// MARK: – Sheet routing
|
|
|
+/// A single setting inside a settings screen, exposed to Menu search. Leaves
|
|
|
+/// are not navigable on their own — a search hit opens the screen
|
|
|
+/// (`SettingsRoute`) that contains it.
|
|
|
+struct SettingsLeaf: Hashable {
|
|
|
+ let title: String
|
|
|
+ let keywords: [String]
|
|
|
+
|
|
|
+ init(_ title: String, _ keywords: [String] = []) {
|
|
|
+ self.title = title
|
|
|
+ self.keywords = keywords
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
enum SettingsRoute: Hashable, Identifiable {
|
|
|
case settings
|
|
|
@@ -125,6 +69,224 @@ enum SettingsRoute: Hashable, Identifiable {
|
|
|
|
|
|
var id: Self { self }
|
|
|
|
|
|
+ // MARK: – Row presentation (single source of truth)
|
|
|
+
|
|
|
+ /// Title shown in the Settings list and used for search matching.
|
|
|
+ /// Non-row cases (`.settings`, `.aggregatedStats`) return "" and are never
|
|
|
+ /// included in `menuSections`.
|
|
|
+ var title: String {
|
|
|
+ switch self {
|
|
|
+ case .nightscout: return "Nightscout"
|
|
|
+ case .dexcom: return "Dexcom"
|
|
|
+ case .general: return "General"
|
|
|
+ case .graph: return "Graph"
|
|
|
+ case .infoDisplay: return "Information Display"
|
|
|
+ case .units: return "Units and Metrics"
|
|
|
+ case .tabSettings: return "Tabs"
|
|
|
+ case .backgroundRefresh: return "Background Refresh"
|
|
|
+ case .importExport: return "Import/Export"
|
|
|
+ case .apn: return "APN"
|
|
|
+ #if !targetEnvironment(macCatalyst)
|
|
|
+ case .liveActivity: return "Live Activity"
|
|
|
+ #endif
|
|
|
+ case .remote: return "Remote"
|
|
|
+ case .alarmSettings: return "Alarms"
|
|
|
+ case .calendar: return "Calendar"
|
|
|
+ case .contact: return "Contact"
|
|
|
+ case .advanced: return "Advanced"
|
|
|
+ case .settings, .aggregatedStats: return ""
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var icon: String {
|
|
|
+ switch self {
|
|
|
+ case .nightscout: return "network"
|
|
|
+ case .dexcom: return "sensor.tag.radiowaves.forward"
|
|
|
+ case .general: return "gearshape"
|
|
|
+ case .graph: return "chart.xyaxis.line"
|
|
|
+ case .infoDisplay: return "info.circle"
|
|
|
+ case .units: return "scalemass"
|
|
|
+ case .tabSettings: return "rectangle.3.group"
|
|
|
+ case .backgroundRefresh: return "arrow.clockwise"
|
|
|
+ case .importExport: return "square.and.arrow.down"
|
|
|
+ case .apn: return "bell.and.waves.left.and.right"
|
|
|
+ #if !targetEnvironment(macCatalyst)
|
|
|
+ case .liveActivity: return "dot.radiowaves.left.and.right"
|
|
|
+ #endif
|
|
|
+ case .remote: return "antenna.radiowaves.left.and.right"
|
|
|
+ case .alarmSettings: return "bell.badge"
|
|
|
+ case .calendar: return "calendar"
|
|
|
+ case .contact: return "person.circle"
|
|
|
+ case .advanced: return "exclamationmark.shield"
|
|
|
+ case .settings, .aggregatedStats: return ""
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Extra synonyms so search finds a page by related terms, not just its title.
|
|
|
+ var keywords: [String] {
|
|
|
+ switch self {
|
|
|
+ case .graph: return ["chart"]
|
|
|
+ case .units: return ["mmol", "mgdl", "metrics"]
|
|
|
+ case .infoDisplay: return ["info"]
|
|
|
+ case .apn: return ["push", "notification"]
|
|
|
+ case .liveActivity: return ["dynamic island", "lock screen"]
|
|
|
+ case .importExport: return ["import", "export", "backup"]
|
|
|
+ default: return []
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// The individual settings inside this screen, so Menu search can find a
|
|
|
+ /// bottom-level setting (e.g. "Graph Basal") and open the screen containing
|
|
|
+ /// it. Titles mirror the row (or section) titles in each screen's view —
|
|
|
+ /// keep them in sync when adding or renaming rows.
|
|
|
+ var leaves: [SettingsLeaf] {
|
|
|
+ switch self {
|
|
|
+ case .nightscout: return [
|
|
|
+ SettingsLeaf("URL"),
|
|
|
+ SettingsLeaf("Access Token", ["token"]),
|
|
|
+ SettingsLeaf("Enable WebSocket", ["websocket", "real-time"]),
|
|
|
+ ]
|
|
|
+ case .dexcom: return [
|
|
|
+ SettingsLeaf("User Name", ["username"]),
|
|
|
+ SettingsLeaf("Password"),
|
|
|
+ SettingsLeaf("Server"),
|
|
|
+ ]
|
|
|
+ case .general: return [
|
|
|
+ SettingsLeaf("Display App Badge", ["badge"]),
|
|
|
+ SettingsLeaf("Persistent Notification"),
|
|
|
+ SettingsLeaf("Appearance", ["dark mode", "light mode", "theme"]),
|
|
|
+ SettingsLeaf("Display Stats"),
|
|
|
+ SettingsLeaf("Display Small Graph"),
|
|
|
+ SettingsLeaf("Color BG Text"),
|
|
|
+ SettingsLeaf("Keep Screen Active", ["screen lock", "screenlock"]),
|
|
|
+ SettingsLeaf("Show Display Name"),
|
|
|
+ SettingsLeaf("Snoozer emoji"),
|
|
|
+ SettingsLeaf("Force portrait mode", ["orientation", "landscape"]),
|
|
|
+ SettingsLeaf("Time Zone Override", ["timezone"]),
|
|
|
+ SettingsLeaf("Speak BG", ["voice", "speech"]),
|
|
|
+ SettingsLeaf("Send anonymous usage stats", ["telemetry", "diagnostics"]),
|
|
|
+ ]
|
|
|
+ case .graph: return [
|
|
|
+ SettingsLeaf("Display Dots"),
|
|
|
+ SettingsLeaf("Display Lines"),
|
|
|
+ SettingsLeaf("Show DIA Lines", ["dia"]),
|
|
|
+ SettingsLeaf("Show −30 min Line", ["-30"]),
|
|
|
+ SettingsLeaf("Show −90 min Line", ["-90"]),
|
|
|
+ SettingsLeaf("Show Yesterday's BG", ["yesterday"]),
|
|
|
+ SettingsLeaf("Show Midnight Lines"),
|
|
|
+ SettingsLeaf("Show Carb/Bolus Values", ["carbs"]),
|
|
|
+ SettingsLeaf("Show Carb Absorption"),
|
|
|
+ SettingsLeaf("Treatments on Small Graph"),
|
|
|
+ SettingsLeaf("Small Graph Height", ["height"]),
|
|
|
+ SettingsLeaf("Hours of Prediction", ["prediction"]),
|
|
|
+ SettingsLeaf("Prediction Style"),
|
|
|
+ SettingsLeaf("Min Basal", ["basal scale"]),
|
|
|
+ SettingsLeaf("Min BG Scale"),
|
|
|
+ SettingsLeaf("Show Days Back", ["history", "days back"]),
|
|
|
+ ]
|
|
|
+ case .infoDisplay:
|
|
|
+ return [SettingsLeaf("Hide Information Table")]
|
|
|
+ + InfoType.allCases.map { SettingsLeaf($0.name) }
|
|
|
+ case .units: return [
|
|
|
+ SettingsLeaf("Glucose Unit"),
|
|
|
+ SettingsLeaf("Range Mode", ["tir", "titr", "time in range"]),
|
|
|
+ SettingsLeaf("Glycemic Metrics", ["hba1c", "ehba1c", "gmi"]),
|
|
|
+ SettingsLeaf("Variability", ["standard deviation", "cv"]),
|
|
|
+ ]
|
|
|
+ case .backgroundRefresh: return [
|
|
|
+ SettingsLeaf("Background Refresh Type", ["silent tune", "bluetooth", "rileylink", "omnipod", "heartbeat"]),
|
|
|
+ ]
|
|
|
+ case .importExport: return [
|
|
|
+ SettingsLeaf("Scan QR Code to Import Settings", ["qr"]),
|
|
|
+ SettingsLeaf("Export Settings To QR Code", ["qr"]),
|
|
|
+ ]
|
|
|
+ case .apn: return [
|
|
|
+ SettingsLeaf("APNS Key ID", ["apns"]),
|
|
|
+ SettingsLeaf("APNS Key", ["apns", "p8"]),
|
|
|
+ ]
|
|
|
+ #if !targetEnvironment(macCatalyst)
|
|
|
+ case .liveActivity: return [
|
|
|
+ SettingsLeaf("Enable Live Activity"),
|
|
|
+ SettingsLeaf("Restart Live Activity"),
|
|
|
+ SettingsLeaf("Grid Slots", ["carplay", "watch"]),
|
|
|
+ ]
|
|
|
+ #endif
|
|
|
+ case .remote: return [
|
|
|
+ SettingsLeaf("Loop Remote Control"),
|
|
|
+ SettingsLeaf("Trio Remote Control", ["trc"]),
|
|
|
+ SettingsLeaf("Meal with Bolus"),
|
|
|
+ SettingsLeaf("Meal with Fat/Protein"),
|
|
|
+ SettingsLeaf("Guardrails", ["max bolus", "max carbs", "max fat", "max protein"]),
|
|
|
+ SettingsLeaf("Bolus Increment"),
|
|
|
+ SettingsLeaf("Shared Secret"),
|
|
|
+ SettingsLeaf("QR Code URL", ["qr"]),
|
|
|
+ ]
|
|
|
+ case .alarmSettings: return [
|
|
|
+ SettingsLeaf("All Alerts Snoozed", ["snooze all"]),
|
|
|
+ SettingsLeaf("All Sounds Muted", ["mute all"]),
|
|
|
+ SettingsLeaf("Day starts", ["schedule", "day/night"]),
|
|
|
+ SettingsLeaf("Night starts", ["schedule", "day/night"]),
|
|
|
+ SettingsLeaf("Override System Volume", ["volume"]),
|
|
|
+ SettingsLeaf("Audio During Calls"),
|
|
|
+ SettingsLeaf("Ignore Zero BG"),
|
|
|
+ SettingsLeaf("Auto-Snooze CGM Start", ["autosnooze"]),
|
|
|
+ SettingsLeaf("Volume Buttons Snooze Alarms"),
|
|
|
+ ]
|
|
|
+ case .calendar: return [
|
|
|
+ SettingsLeaf("Save BG to Calendar", ["watch", "carplay"]),
|
|
|
+ SettingsLeaf("Calendar Text"),
|
|
|
+ ]
|
|
|
+ case .contact: return [
|
|
|
+ SettingsLeaf("Enable Contact BG Updates", ["watch face"]),
|
|
|
+ SettingsLeaf("Background Color"),
|
|
|
+ SettingsLeaf("Color Mode"),
|
|
|
+ SettingsLeaf("Text Color"),
|
|
|
+ SettingsLeaf("Show Trend"),
|
|
|
+ SettingsLeaf("Show Delta"),
|
|
|
+ SettingsLeaf("Show IOB"),
|
|
|
+ ]
|
|
|
+ case .advanced: return [
|
|
|
+ SettingsLeaf("Download Treatments"),
|
|
|
+ SettingsLeaf("Download Prediction"),
|
|
|
+ SettingsLeaf("Graph Basal"),
|
|
|
+ SettingsLeaf("Graph Bolus"),
|
|
|
+ SettingsLeaf("Graph Carbs"),
|
|
|
+ SettingsLeaf("Graph Other Treatments"),
|
|
|
+ SettingsLeaf("BG Update Delay", ["delay"]),
|
|
|
+ SettingsLeaf("Debug Log Level", ["logging"]),
|
|
|
+ ]
|
|
|
+ case .tabSettings, .settings, .aggregatedStats: return []
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Ordered, grouped list of the routes shown as rows in the Settings menu.
|
|
|
+ /// Encodes the conditional visibility in one place so both the list and the
|
|
|
+ /// Menu search stay in sync.
|
|
|
+ static func menuSections(nightscoutConfigured: Bool) -> [(SettingsSection, [SettingsRoute])] {
|
|
|
+ var display: [SettingsRoute] = [.general, .graph]
|
|
|
+ if nightscoutConfigured {
|
|
|
+ display.append(.infoDisplay)
|
|
|
+ }
|
|
|
+ display += [.units, .tabSettings]
|
|
|
+
|
|
|
+ var app: [SettingsRoute] = [.backgroundRefresh, .importExport, .apn]
|
|
|
+ #if !targetEnvironment(macCatalyst)
|
|
|
+ app.append(.liveActivity)
|
|
|
+ #endif
|
|
|
+ if nightscoutConfigured {
|
|
|
+ app.append(.remote)
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ (.data, [.nightscout, .dexcom]),
|
|
|
+ (.display, display),
|
|
|
+ (.app, app),
|
|
|
+ (.alarms, [.alarmSettings]),
|
|
|
+ (.integrations, [.calendar, .contact]),
|
|
|
+ (.advanced, [.advanced]),
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
@ViewBuilder
|
|
|
var destination: some View {
|
|
|
switch self {
|