SettingsMenuView.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. // 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 !nightscoutURL.value.isEmpty {
  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. }
  133. @ViewBuilder
  134. private var buildInfoSection: some View {
  135. let build = BuildDetails.default
  136. let ver = AppVersionManager().version()
  137. Section("Build Information") {
  138. keyValue("Version", ver, tint: versionTint)
  139. keyValue("Latest version", latestVersion ?? "Fetching…")
  140. if !(build.isMacApp() || build.isSimulatorBuild()) {
  141. keyValue(build.expirationHeaderString,
  142. dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
  143. }
  144. keyValue("Built",
  145. dateTimeUtils.formattedDate(from: build.buildDate()))
  146. keyValue("Branch", build.branchAndSha)
  147. }
  148. }
  149. // MARK: – Helpers
  150. private func keyValue(_ key: String,
  151. _ value: String,
  152. tint: Color = .secondary) -> some View
  153. {
  154. HStack {
  155. Text(key)
  156. Spacer()
  157. Text(value).foregroundColor(tint)
  158. }
  159. }
  160. private func refreshVersionInfo() async {
  161. let mgr = AppVersionManager()
  162. let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
  163. latestVersion = latest ?? "Unknown"
  164. let current = mgr.version()
  165. versionTint = blacklisted ? .red
  166. : newer ? .orange
  167. : latest == current ? .green
  168. : .secondary
  169. }
  170. private func shareLogs() {
  171. let files = LogManager.shared.logFilesForTodayAndYesterday()
  172. guard !files.isEmpty else {
  173. UIApplication.shared.topMost?.presentSimpleAlert(
  174. title: "No Logs Available",
  175. message: "There are no logs to share."
  176. )
  177. return
  178. }
  179. let avc = UIActivityViewController(activityItems: files,
  180. applicationActivities: nil)
  181. UIApplication.shared.topMost?.present(avc, animated: true)
  182. }
  183. }
  184. // MARK: – Sheet routing
  185. private enum Sheet: Hashable, Identifiable {
  186. case nightscout, dexcom
  187. case backgroundRefresh
  188. case general, graph
  189. case infoDisplay
  190. case alarmsList, alarmSettings
  191. case remote
  192. case calendar, contact
  193. case advanced
  194. case viewLog
  195. var id: Self { self }
  196. @ViewBuilder
  197. var destination: some View {
  198. switch self {
  199. case .nightscout: NightscoutSettingsView(viewModel: .init())
  200. case .dexcom: DexcomSettingsView(viewModel: .init())
  201. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  202. case .general: GeneralSettingsView()
  203. case .graph: GraphSettingsView()
  204. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  205. case .alarmsList: AlarmListView()
  206. case .alarmSettings: AlarmSettingsView()
  207. case .remote: RemoteSettingsView(viewModel: .init())
  208. case .calendar: CalendarSettingsView()
  209. case .contact: ContactSettingsView(viewModel: .init())
  210. case .advanced: AdvancedSettingsView(viewModel: .init())
  211. case .viewLog: LogView(viewModel: .init())
  212. }
  213. }
  214. }
  215. // MARK: – UIKit helpers (unchanged)
  216. import UIKit
  217. extension UIApplication {
  218. var topMost: UIViewController? {
  219. guard var top = keyWindow?.rootViewController else { return nil }
  220. while let presented = top.presentedViewController {
  221. top = presented
  222. }
  223. return top
  224. }
  225. }
  226. extension UIViewController {
  227. func presentSimpleAlert(title: String, message: String) {
  228. let a = UIAlertController(title: title,
  229. message: message,
  230. preferredStyle: .alert)
  231. a.addAction(UIAlertAction(title: "OK", style: .default))
  232. present(a, animated: true)
  233. }
  234. }