SettingsMenuView.swift 11 KB

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