| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- // LoopFollow
- // SettingsMenuView.swift
- import SwiftUI
- import UIKit
- struct SettingsMenuView: View {
- @ObservedObject private var nightscoutURL = Storage.shared.url
- var body: some View {
- List {
- 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)
- }
- }
- }
- }
- .navigationTitle("Settings")
- .navigationBarTitleDisplayMode(.large)
- }
- }
- // MARK: – Sheet routing
- 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"
- }
- /// 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
- case units
- case nightscout, dexcom
- case backgroundRefresh
- case general, graph
- case tabSettings
- case infoDisplay
- case alarmSettings
- case apn
- #if !targetEnvironment(macCatalyst)
- case liveActivity
- #endif
- case remote
- case importExport
- case calendar, contact
- case advanced
- case aggregatedStats
- 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"]
- #if !targetEnvironment(macCatalyst)
- case .liveActivity: return ["dynamic island", "lock screen"]
- #endif
- 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 {
- case .settings: SettingsMenuView()
- case .units: UnitsSettingsView()
- case .nightscout: NightscoutSettingsView(viewModel: .init())
- case .dexcom: DexcomSettingsView(viewModel: .init())
- case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
- case .general: GeneralSettingsView()
- case .graph: GraphSettingsView()
- case .tabSettings: TabCustomizationModal()
- case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
- case .alarmSettings: AlarmSettingsView()
- case .apn: APNSettingsView()
- #if !targetEnvironment(macCatalyst)
- case .liveActivity: LiveActivitySettingsView()
- #endif
- case .remote: RemoteSettingsView(viewModel: .init())
- case .importExport: ImportExportSettingsView()
- case .calendar: CalendarSettingsView()
- case .contact: ContactSettingsView(viewModel: .init())
- case .advanced: AdvancedSettingsView(viewModel: .init())
- case .aggregatedStats:
- AggregatedStatsViewWrapper()
- }
- }
- }
- // Helper view to access MainViewController
- struct AggregatedStatsViewWrapper: View {
- @State private var mainViewController: MainViewController?
- var body: some View {
- Group {
- if let mainVC = mainViewController {
- AggregatedStatsContentView(mainViewController: mainVC)
- } else {
- Text("Loading stats...")
- .onAppear {
- mainViewController = getMainViewController()
- }
- }
- }
- }
- private func getMainViewController() -> MainViewController? {
- MainViewController.shared
- }
- }
- // MARK: – UIKit helpers (unchanged)
- extension UIApplication {
- var topMost: UIViewController? {
- // `keyWindow` is deprecated and returns nil on Mac Catalyst / multi-window iPad.
- // Walk connected scenes instead and prefer the foreground-active one.
- let windowScenes = connectedScenes.compactMap { $0 as? UIWindowScene }
- let activeScene = windowScenes.first { $0.activationState == .foregroundActive }
- ?? windowScenes.first
- let rootVC = activeScene?.windows.first(where: \.isKeyWindow)?.rootViewController
- ?? activeScene?.windows.first?.rootViewController
- guard var top = rootVC else { return nil }
- while let presented = top.presentedViewController {
- top = presented
- }
- return top
- }
- }
- extension UIViewController {
- func presentSimpleAlert(title: String, message: String) {
- let a = UIAlertController(title: title,
- message: message,
- preferredStyle: .alert)
- a.addAction(UIAlertAction(title: "OK", style: .default))
- present(a, animated: true)
- }
- }
|