SettingsMenuView.swift 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. // LoopFollow
  2. // SettingsMenuView.swift
  3. // Created by Jonas Björkert on 2025-05-26.
  4. import SwiftUI
  5. import UIKit
  6. struct SettingsMenuView: View {
  7. // MARK: – Call-backs
  8. let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void
  9. // MARK: – Local state
  10. @State private var path = NavigationPath()
  11. @State private var latestVersion: String?
  12. @State private var versionTint: Color = .secondary
  13. @State private var navPath = NavigationPath()
  14. // MARK: – Body
  15. var body: some View {
  16. NavigationStack(path: $path) {
  17. List {
  18. // ───────── Data settings ─────────
  19. dataSection
  20. // ───────── App settings ─────────
  21. Section("App Settings") {
  22. NavigationRow(title: "Background Refresh Settings",
  23. icon: "arrow.clockwise")
  24. {
  25. path.append(Sheet.backgroundRefresh)
  26. }
  27. NavigationRow(title: "General Settings",
  28. icon: "gearshape")
  29. {
  30. path.append(Sheet.general)
  31. }
  32. NavigationRow(title: "Graph Settings",
  33. icon: "chart.xyaxis.line")
  34. {
  35. path.append(Sheet.graph)
  36. }
  37. if IsNightscoutEnabled() {
  38. NavigationRow(title: "Information Display Settings",
  39. icon: "info.circle")
  40. {
  41. path.append(Sheet.infoDisplay)
  42. }
  43. NavigationRow(title: "Remote Settings",
  44. icon: "antenna.radiowaves.left.and.right")
  45. {
  46. path.append(Sheet.remote)
  47. }
  48. }
  49. }
  50. // ───────── Alarms ─────────
  51. Section {
  52. NavigationRow(title: "Alarms",
  53. icon: "bell")
  54. {
  55. path.append(Sheet.alarmsList)
  56. }
  57. NavigationRow(title: "Alarm Settings",
  58. icon: "bell.badge")
  59. {
  60. path.append(Sheet.alarmSettings)
  61. }
  62. }
  63. // ───────── Integrations ─────────
  64. Section("Integrations") {
  65. NavigationRow(title: "Calendar",
  66. icon: "calendar")
  67. {
  68. path.append(Sheet.calendar)
  69. }
  70. NavigationRow(title: "Contact",
  71. icon: "person.circle")
  72. {
  73. path.append(Sheet.contact)
  74. }
  75. }
  76. // ───────── Advanced / Logs ─────────
  77. Section("Advanced Settings") {
  78. NavigationRow(title: "Advanced Settings",
  79. icon: "exclamationmark.shield")
  80. {
  81. path.append(Sheet.advanced)
  82. }
  83. }
  84. Section("Logging") {
  85. NavigationRow(title: "View Log",
  86. icon: "doc.text.magnifyingglass")
  87. {
  88. path.append(Sheet.viewLog)
  89. }
  90. ActionRow(title: "Share Logs",
  91. icon: "square.and.arrow.up",
  92. action: shareLogs)
  93. }
  94. // ───────── Community ─────────
  95. Section("Community") {
  96. LinkRow(title: "LoopFollow Facebook Group",
  97. icon: "person.2.fill",
  98. url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!)
  99. }
  100. // ───────── Build info ─────────
  101. buildInfoSection
  102. }
  103. .navigationTitle("Settings")
  104. .navigationDestination(for: Sheet.self) { $0.destination }
  105. }
  106. .task { await refreshVersionInfo() }
  107. }
  108. // MARK: – Section builders
  109. @ViewBuilder
  110. private var dataSection: some View {
  111. Section("Data Settings") {
  112. Picker("Units",
  113. selection: Binding(
  114. get: { Storage.shared.units.value },
  115. set: { Storage.shared.units.value = $0 }
  116. )) {
  117. Text("mg/dL").tag("mg/dL")
  118. Text("mmol/L").tag("mmol/L")
  119. }
  120. .pickerStyle(.segmented)
  121. NavigationRow(title: "Nightscout Settings",
  122. icon: "network")
  123. {
  124. path.append(Sheet.nightscout)
  125. }
  126. NavigationRow(title: "Dexcom Settings",
  127. icon: "sensor.tag.radiowaves.forward")
  128. {
  129. path.append(Sheet.dexcom)
  130. }
  131. }
  132. .onAppear {
  133. onNightscoutVisibilityChange(IsNightscoutEnabled())
  134. }
  135. }
  136. @ViewBuilder
  137. private var buildInfoSection: some View {
  138. let build = BuildDetails.default
  139. let ver = AppVersionManager().version()
  140. Section("Build Information") {
  141. keyValue("Version", ver, tint: versionTint)
  142. keyValue("Latest version", latestVersion ?? "Fetching…")
  143. if !(build.isMacApp() || build.isSimulatorBuild()) {
  144. keyValue(build.expirationHeaderString,
  145. dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
  146. }
  147. keyValue("Built",
  148. dateTimeUtils.formattedDate(from: build.buildDate()))
  149. keyValue("Branch", build.branchAndSha)
  150. }
  151. }
  152. // MARK: – Helpers
  153. private func keyValue(_ key: String,
  154. _ value: String,
  155. tint: Color = .secondary) -> some View
  156. {
  157. HStack {
  158. Text(key)
  159. Spacer()
  160. Text(value).foregroundColor(tint)
  161. }
  162. }
  163. private func refreshVersionInfo() async {
  164. let mgr = AppVersionManager()
  165. let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
  166. latestVersion = latest ?? "Unknown"
  167. let current = mgr.version()
  168. versionTint = blacklisted ? .red
  169. : newer ? .orange
  170. : latest == current ? .green
  171. : .secondary
  172. }
  173. private func shareLogs() {
  174. let files = LogManager.shared.logFilesForTodayAndYesterday()
  175. guard !files.isEmpty else {
  176. UIApplication.shared.topMost?.presentSimpleAlert(
  177. title: "No Logs Available",
  178. message: "There are no logs to share."
  179. )
  180. return
  181. }
  182. let avc = UIActivityViewController(activityItems: files,
  183. applicationActivities: nil)
  184. UIApplication.shared.topMost?.present(avc, animated: true)
  185. }
  186. }
  187. // MARK: – Sheet routing
  188. private enum Sheet: Hashable, Identifiable {
  189. case nightscout, dexcom
  190. case backgroundRefresh
  191. case general, graph
  192. case infoDisplay
  193. case alarmsList, alarmSettings
  194. case remote
  195. case calendar, contact
  196. case advanced
  197. case viewLog
  198. var id: Self { self }
  199. @ViewBuilder
  200. var destination: some View {
  201. switch self {
  202. case .nightscout: NightscoutSettingsView(viewModel: .init())
  203. case .dexcom: DexcomSettingsView(viewModel: .init())
  204. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  205. case .general: GeneralSettingsView()
  206. case .graph: GraphSettingsView()
  207. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  208. case .alarmsList: AlarmListView()
  209. case .alarmSettings: AlarmSettingsView()
  210. case .remote: RemoteSettingsView(viewModel: .init())
  211. case .calendar: CalendarSettingsView()
  212. case .contact: ContactSettingsView(viewModel: .init())
  213. case .advanced: AdvancedSettingsView(viewModel: .init())
  214. case .viewLog: LogView(viewModel: .init())
  215. }
  216. }
  217. }
  218. // MARK: – UIKit helpers (unchanged)
  219. import UIKit
  220. extension UIApplication {
  221. var topMost: UIViewController? {
  222. guard var top = keyWindow?.rootViewController else { return nil }
  223. while let presented = top.presentedViewController {
  224. top = presented
  225. }
  226. return top
  227. }
  228. }
  229. extension UIViewController {
  230. func presentSimpleAlert(title: String, message: String) {
  231. let a = UIAlertController(title: title,
  232. message: message,
  233. preferredStyle: .alert)
  234. a.addAction(UIAlertAction(title: "OK", style: .default))
  235. present(a, animated: true)
  236. }
  237. }