SettingsMenuView.swift 11 KB

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