SettingsMenuView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. // LoopFollow
  2. // SettingsMenuView.swift
  3. import SwiftUI
  4. import UIKit
  5. struct SettingsMenuView: View {
  6. // MARK: - Observed Objects
  7. @ObservedObject private var nightscoutURL = Storage.shared.url
  8. @ObservedObject private var settingsPath = Observable.shared.settingsPath
  9. // MARK: – Local state
  10. @State private var latestVersion: String?
  11. @State private var versionTint: Color = .secondary
  12. @State private var showingTabCustomization = false
  13. // MARK: – Observed objects
  14. @ObservedObject private var url = Storage.shared.url
  15. // MARK: – Body
  16. var body: some View {
  17. NavigationStack(path: $settingsPath.value) {
  18. List {
  19. // ───────── Data settings ─────────
  20. dataSection
  21. // ───────── App settings ─────────
  22. Section("App Settings") {
  23. NavigationRow(title: "Background Refresh Settings",
  24. icon: "arrow.clockwise")
  25. {
  26. settingsPath.value.append(Sheet.backgroundRefresh)
  27. }
  28. NavigationRow(title: "General Settings",
  29. icon: "gearshape")
  30. {
  31. settingsPath.value.append(Sheet.general)
  32. }
  33. NavigationRow(title: "Graph Settings",
  34. icon: "chart.xyaxis.line")
  35. {
  36. settingsPath.value.append(Sheet.graph)
  37. }
  38. NavigationRow(title: "Tab Settings",
  39. icon: "rectangle.3.group")
  40. {
  41. showingTabCustomization = true
  42. }
  43. NavigationRow(title: "Import/Export Settings",
  44. icon: "square.and.arrow.down")
  45. {
  46. settingsPath.value.append(Sheet.importExport)
  47. }
  48. if !nightscoutURL.value.isEmpty {
  49. NavigationRow(title: "Information Display Settings",
  50. icon: "info.circle")
  51. {
  52. settingsPath.value.append(Sheet.infoDisplay)
  53. }
  54. NavigationRow(title: "Remote Settings",
  55. icon: "antenna.radiowaves.left.and.right")
  56. {
  57. settingsPath.value.append(Sheet.remote)
  58. }
  59. }
  60. }
  61. // ───────── Alarms ─────────
  62. Section("Alarms") {
  63. NavigationRow(title: "Alarm Settings",
  64. icon: "bell.badge")
  65. {
  66. settingsPath.value.append(Sheet.alarmSettings)
  67. }
  68. }
  69. // ───────── Integrations ─────────
  70. Section("Integrations") {
  71. NavigationRow(title: "Calendar",
  72. icon: "calendar")
  73. {
  74. settingsPath.value.append(Sheet.calendar)
  75. }
  76. NavigationRow(title: "Contact",
  77. icon: "person.circle")
  78. {
  79. settingsPath.value.append(Sheet.contact)
  80. }
  81. }
  82. // ───────── Advanced / Logs ─────────
  83. Section("Advanced Settings") {
  84. NavigationRow(title: "Advanced Settings",
  85. icon: "exclamationmark.shield")
  86. {
  87. settingsPath.value.append(Sheet.advanced)
  88. }
  89. }
  90. Section("Logging") {
  91. NavigationRow(title: "View Log",
  92. icon: "doc.text.magnifyingglass")
  93. {
  94. settingsPath.value.append(Sheet.viewLog)
  95. }
  96. ActionRow(title: "Share Logs",
  97. icon: "square.and.arrow.up",
  98. action: shareLogs)
  99. }
  100. // ───────── Build info ─────────
  101. buildInfoSection
  102. }
  103. .navigationTitle("Settings")
  104. .navigationDestination(for: Sheet.self) { $0.destination }
  105. .sheet(isPresented: $showingTabCustomization) {
  106. TabCustomizationModal(
  107. isPresented: $showingTabCustomization,
  108. onApply: {
  109. // No-op - changes are applied silently via observers
  110. }
  111. )
  112. }
  113. }
  114. .task { await refreshVersionInfo() }
  115. }
  116. // MARK: – Section builders
  117. @ViewBuilder
  118. private var dataSection: some View {
  119. Section("Data Settings") {
  120. Picker("Units",
  121. selection: Binding(
  122. get: { Storage.shared.units.value },
  123. set: { Storage.shared.units.value = $0 }
  124. )) {
  125. Text("mg/dL").tag("mg/dL")
  126. Text("mmol/L").tag("mmol/L")
  127. }
  128. .pickerStyle(.segmented)
  129. NavigationRow(title: "Nightscout Settings",
  130. icon: "network")
  131. {
  132. settingsPath.value.append(Sheet.nightscout)
  133. }
  134. NavigationRow(title: "Dexcom Settings",
  135. icon: "sensor.tag.radiowaves.forward")
  136. {
  137. settingsPath.value.append(Sheet.dexcom)
  138. }
  139. }
  140. }
  141. @ViewBuilder
  142. private var buildInfoSection: some View {
  143. let build = BuildDetails.default
  144. let ver = AppVersionManager().version()
  145. Section("Build Information") {
  146. keyValue("Version", ver, tint: versionTint)
  147. keyValue("Latest version", latestVersion ?? "Fetching…")
  148. if !(build.isMacApp() || build.isSimulatorBuild()) {
  149. keyValue(build.expirationHeaderString,
  150. dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
  151. }
  152. keyValue("Built",
  153. dateTimeUtils.formattedDate(from: build.buildDate()))
  154. keyValue("Branch", build.branchAndSha)
  155. }
  156. }
  157. // MARK: – Helpers
  158. private func keyValue(_ key: String,
  159. _ value: String,
  160. tint: Color = .secondary) -> some View
  161. {
  162. HStack {
  163. Text(key)
  164. Spacer()
  165. Text(value).foregroundColor(tint)
  166. }
  167. }
  168. private func refreshVersionInfo() async {
  169. let mgr = AppVersionManager()
  170. let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
  171. latestVersion = latest ?? "Unknown"
  172. let current = mgr.version()
  173. versionTint = blacklisted ? .red
  174. : newer ? .orange
  175. : latest == current ? .green
  176. : .secondary
  177. }
  178. private func shareLogs() {
  179. let files = LogManager.shared.logFilesForTodayAndYesterday()
  180. guard !files.isEmpty else {
  181. UIApplication.shared.topMost?.presentSimpleAlert(
  182. title: "No Logs Available",
  183. message: "There are no logs to share."
  184. )
  185. return
  186. }
  187. let avc = UIActivityViewController(activityItems: files,
  188. applicationActivities: nil)
  189. UIApplication.shared.topMost?.present(avc, animated: true)
  190. }
  191. private func handleTabReorganization() {
  192. // Rebuild the tab bar with the new configuration
  193. MainViewController.rebuildTabsIfNeeded()
  194. }
  195. }
  196. // MARK: – Sheet routing
  197. private enum Sheet: Hashable, Identifiable {
  198. case nightscout, dexcom
  199. case backgroundRefresh
  200. case general, graph
  201. case infoDisplay
  202. case alarmSettings
  203. case remote
  204. case importExport
  205. case calendar, contact
  206. case advanced
  207. case viewLog
  208. case aggregatedStats
  209. var id: Self { self }
  210. @ViewBuilder
  211. var destination: some View {
  212. switch self {
  213. case .nightscout: NightscoutSettingsView(viewModel: .init())
  214. case .dexcom: DexcomSettingsView(viewModel: .init())
  215. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  216. case .general: GeneralSettingsView()
  217. case .graph: GraphSettingsView()
  218. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  219. case .alarmSettings: AlarmSettingsView()
  220. case .remote: RemoteSettingsView(viewModel: .init())
  221. case .importExport: ImportExportSettingsView()
  222. case .calendar: CalendarSettingsView()
  223. case .contact: ContactSettingsView(viewModel: .init())
  224. case .advanced: AdvancedSettingsView(viewModel: .init())
  225. case .viewLog: LogView(viewModel: .init())
  226. case .aggregatedStats:
  227. AggregatedStatsViewWrapper()
  228. }
  229. }
  230. }
  231. // Helper view to access MainViewController
  232. struct AggregatedStatsViewWrapper: View {
  233. @State private var mainViewController: MainViewController?
  234. var body: some View {
  235. Group {
  236. if let mainVC = mainViewController {
  237. AggregatedStatsView(viewModel: AggregatedStatsViewModel(mainViewController: mainVC))
  238. } else {
  239. Text("Loading stats...")
  240. .onAppear {
  241. mainViewController = getMainViewController()
  242. }
  243. }
  244. }
  245. }
  246. private func getMainViewController() -> MainViewController? {
  247. guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
  248. let window = windowScene.windows.first,
  249. let rootVC = window.rootViewController
  250. else {
  251. return nil
  252. }
  253. if let mainVC = rootVC as? MainViewController {
  254. return mainVC
  255. }
  256. if let navVC = rootVC as? UINavigationController,
  257. let mainVC = navVC.viewControllers.first as? MainViewController
  258. {
  259. return mainVC
  260. }
  261. if let tabVC = rootVC as? UITabBarController {
  262. for vc in tabVC.viewControllers ?? [] {
  263. if let mainVC = vc as? MainViewController {
  264. return mainVC
  265. }
  266. if let navVC = vc as? UINavigationController,
  267. let mainVC = navVC.viewControllers.first as? MainViewController
  268. {
  269. return mainVC
  270. }
  271. }
  272. }
  273. return nil
  274. }
  275. }
  276. // MARK: – UIKit helpers (unchanged)
  277. import UIKit
  278. extension UIApplication {
  279. var topMost: UIViewController? {
  280. guard var top = keyWindow?.rootViewController else { return nil }
  281. while let presented = top.presentedViewController {
  282. top = presented
  283. }
  284. return top
  285. }
  286. }
  287. extension UIViewController {
  288. func presentSimpleAlert(title: String, message: String) {
  289. let a = UIAlertController(title: title,
  290. message: message,
  291. preferredStyle: .alert)
  292. a.addAction(UIAlertAction(title: "OK", style: .default))
  293. present(a, animated: true)
  294. }
  295. }