SettingsMenuView.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // LoopFollow
  2. // SettingsMenuView.swift
  3. // Created by Jonas Björkert on 2025-05-26.
  4. import SwiftUI
  5. import UIKit
  6. struct SettingsMenuView: View {
  7. // MARK: – Call-backs
  8. let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void
  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 IsNightscoutEnabled() {
  38. NavigationRow(title: "Information Display Settings",
  39. icon: "info.circle")
  40. {
  41. path.append(Sheet.infoDisplay)
  42. }
  43. }
  44. }
  45. // ───────── Alarms ─────────
  46. Section {
  47. NavigationRow(title: "Alarms",
  48. icon: "bell")
  49. {
  50. path.append(Sheet.alarmsList)
  51. }
  52. NavigationRow(title: "Alarm Settings",
  53. icon: "bell.badge")
  54. {
  55. path.append(Sheet.alarmSettings)
  56. }
  57. }
  58. // ───────── Integrations ─────────
  59. Section("Integrations") {
  60. NavigationRow(title: "Calendar",
  61. icon: "calendar")
  62. {
  63. path.append(Sheet.calendar)
  64. }
  65. NavigationRow(title: "Contact",
  66. icon: "person.circle")
  67. {
  68. path.append(Sheet.contact)
  69. }
  70. }
  71. // ───────── Advanced / Logs ─────────
  72. Section("Advanced Settings") {
  73. NavigationRow(title: "Advanced Settings",
  74. icon: "exclamationmark.shield")
  75. {
  76. path.append(Sheet.advanced)
  77. }
  78. }
  79. Section("Logging") {
  80. NavigationRow(title: "View Log",
  81. icon: "doc.text.magnifyingglass")
  82. {
  83. path.append(Sheet.viewLog)
  84. }
  85. ActionRow(title: "Share Logs",
  86. icon: "square.and.arrow.up",
  87. action: shareLogs)
  88. }
  89. // ───────── Community ─────────
  90. Section("Community") {
  91. LinkRow(title: "LoopFollow Facebook Group",
  92. icon: "person.2.fill",
  93. url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!)
  94. }
  95. // ───────── Build info ─────────
  96. buildInfoSection
  97. }
  98. .navigationTitle("Settings")
  99. .navigationDestination(for: Sheet.self) { $0.destination }
  100. }
  101. .task { await refreshVersionInfo() }
  102. }
  103. // MARK: – Section builders
  104. @ViewBuilder
  105. private var dataSection: some View {
  106. Section("Data Settings") {
  107. Picker("Units",
  108. selection: Binding(
  109. get: { UserDefaultsRepository.units.value },
  110. set: { UserDefaultsRepository.units.value = $0 }
  111. )) {
  112. Text("mg/dL").tag("mg/dL")
  113. Text("mmol/L").tag("mmol/L")
  114. }
  115. .pickerStyle(.segmented)
  116. NavigationRow(title: "Nightscout Settings",
  117. icon: "network")
  118. {
  119. path.append(Sheet.nightscout)
  120. }
  121. NavigationRow(title: "Dexcom Settings",
  122. icon: "sensor.tag.radiowaves.forward")
  123. {
  124. path.append(Sheet.dexcom)
  125. }
  126. }
  127. .onAppear {
  128. onNightscoutVisibilityChange(IsNightscoutEnabled())
  129. }
  130. }
  131. @ViewBuilder
  132. private var buildInfoSection: some View {
  133. let build = BuildDetails.default
  134. let ver = AppVersionManager().version()
  135. Section("Build Information") {
  136. keyValue("Version", ver, tint: versionTint)
  137. keyValue("Latest version", latestVersion ?? "Fetching…")
  138. if !(build.isMacApp() || build.isSimulatorBuild()) {
  139. keyValue(build.expirationHeaderString,
  140. dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
  141. }
  142. keyValue("Built",
  143. dateTimeUtils.formattedDate(from: build.buildDate()))
  144. keyValue("Branch", build.branchAndSha)
  145. }
  146. }
  147. // MARK: – Helpers
  148. private func keyValue(_ key: String,
  149. _ value: String,
  150. tint: Color = .secondary) -> some View
  151. {
  152. HStack {
  153. Text(key)
  154. Spacer()
  155. Text(value).foregroundColor(tint)
  156. }
  157. }
  158. private func refreshVersionInfo() async {
  159. let mgr = AppVersionManager()
  160. let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
  161. latestVersion = latest ?? "Unknown"
  162. let current = mgr.version()
  163. versionTint = blacklisted ? .red
  164. : newer ? .orange
  165. : latest == current ? .green
  166. : .secondary
  167. }
  168. private func shareLogs() {
  169. let files = LogManager.shared.logFilesForTodayAndYesterday()
  170. guard !files.isEmpty else {
  171. UIApplication.shared.topMost?.presentSimpleAlert(
  172. title: "No Logs Available",
  173. message: "There are no logs to share."
  174. )
  175. return
  176. }
  177. let avc = UIActivityViewController(activityItems: files,
  178. applicationActivities: nil)
  179. UIApplication.shared.topMost?.present(avc, animated: true)
  180. }
  181. }
  182. // MARK: – Sheet routing
  183. private enum Sheet: Hashable, Identifiable {
  184. case nightscout, dexcom
  185. case backgroundRefresh
  186. case general, graph
  187. case infoDisplay
  188. case alarmsList, alarmSettings
  189. case remote
  190. case calendar, contact
  191. case advanced
  192. case viewLog
  193. var id: Self { self }
  194. @ViewBuilder
  195. var destination: some View {
  196. switch self {
  197. case .nightscout: NightscoutSettingsView(viewModel: .init())
  198. case .dexcom: DexcomSettingsView(viewModel: .init())
  199. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  200. case .general: GeneralSettingsView()
  201. case .graph: GraphSettingsView()
  202. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  203. case .alarmsList: AlarmListView()
  204. case .alarmSettings: AlarmSettingsView()
  205. case .remote: RemoteSettingsView(viewModel: .init())
  206. case .calendar: CalendarSettingsView()
  207. case .contact: ContactSettingsView(viewModel: .init())
  208. case .advanced: AdvancedSettingsView(viewModel: .init())
  209. case .viewLog: LogView(viewModel: .init())
  210. }
  211. }
  212. }
  213. // MARK: – UIKit helpers (unchanged)
  214. import UIKit
  215. extension UIApplication {
  216. var topMost: UIViewController? {
  217. guard var top = keyWindow?.rootViewController else { return nil }
  218. while let presented = top.presentedViewController {
  219. top = presented
  220. }
  221. return top
  222. }
  223. }
  224. extension UIViewController {
  225. func presentSimpleAlert(title: String, message: String) {
  226. let a = UIAlertController(title: title,
  227. message: message,
  228. preferredStyle: .alert)
  229. a.addAction(UIAlertAction(title: "OK", style: .default))
  230. present(a, animated: true)
  231. }
  232. }