SettingsMenuView.swift 9.6 KB

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