Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion LoopFollow/Helpers/Views/NavigationRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SwiftUI

struct NavigationRow<Value: Hashable>: View {
let title: String
var subtitle: String? = nil
let icon: String
var iconTint: Color = .white
let value: Value
Expand All @@ -13,7 +14,14 @@ struct NavigationRow<Value: Hashable>: View {
NavigationLink(value: value) {
HStack {
Glyph(symbol: icon, tint: iconTint)
Text(title)
VStack(alignment: .leading, spacing: 2) {
Text(title)
if let subtitle {
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
}
Expand Down
330 changes: 246 additions & 84 deletions LoopFollow/Settings/SettingsMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Loading
Loading