SettingsMenuView.swift 11 KB

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