SettingsMenuView.swift 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. // LoopFollow
  2. // SettingsMenuView.swift
  3. // Created by Jonas Björkert on 2025-05-26.
  4. import SwiftUI
  5. struct SettingsMenuView: View {
  6. // MARK: – Call-backs -----------------------------------------------------
  7. let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void
  8. // MARK: – Local state ----------------------------------------------------
  9. @State private var sheet: Sheet?
  10. @State private var latestVersion: String?
  11. @State private var versionTint: Color = .secondary
  12. // MARK: – Body -----------------------------------------------------------
  13. var body: some View {
  14. NavigationStack {
  15. List {
  16. // ────────────── Data settings ──────────────
  17. dataSection
  18. // ────────────── App settings ──────────────
  19. Section("App Settings") {
  20. navRow(title: "Background Refresh Settings",
  21. icon: "arrow.clockwise",
  22. destination: .backgroundRefresh)
  23. navRow(title: "General Settings",
  24. icon: "gearshape",
  25. destination: .general)
  26. navRow(title: "Graph Settings",
  27. icon: "chart.xyaxis.line",
  28. destination: .graph)
  29. if IsNightscoutEnabled() {
  30. navRow(title: "Information Display Settings",
  31. icon: "info.circle",
  32. destination: .infoDisplay)
  33. }
  34. }
  35. // ────────────── Alarms ──────────────
  36. Section {
  37. navRow(title: "Alarms",
  38. icon: "bell",
  39. destination: .alarmsList)
  40. navRow(title: "Alarm Settings",
  41. icon: "bell.badge",
  42. destination: .alarmSettings)
  43. }
  44. // ────────────── Integrations ──────────────
  45. Section("Integrations") {
  46. navRow(title: "Calendar",
  47. icon: "calendar",
  48. destination: .calendar)
  49. navRow(title: "Contact",
  50. icon: "envelope",
  51. destination: .contact)
  52. }
  53. // ────────────── Advanced / Logs ──────────────
  54. Section("Advanced Settings") {
  55. navRow(title: "Advanced Settings",
  56. icon: "exclamationmark.shield",
  57. destination: .advanced)
  58. }
  59. Section("Logging") {
  60. navRow(title: "View Log",
  61. icon: "doc.text.magnifyingglass",
  62. destination: .viewLog)
  63. Button {
  64. shareLogs()
  65. } label: {
  66. Label("Share Logs", systemImage: "square.and.arrow.up")
  67. }
  68. .accessibilityIdentifier("ShareLogsButton")
  69. }
  70. // ────────────── Community ──────────────
  71. Section("Community") {
  72. Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) {
  73. Label("LoopFollow Facebook Group", systemImage: "person.3")
  74. }
  75. }
  76. // ────────────── Build info ──────────────
  77. buildInfoSection
  78. }
  79. .navigationTitle("Settings")
  80. }
  81. .task { await refreshVersionInfo() }
  82. .sheet(item: $sheet) { $0.destination }
  83. }
  84. // MARK: – Section builders ----------------------------------------------
  85. /// “Data Settings”
  86. @ViewBuilder
  87. private var dataSection: some View {
  88. Section("Data Settings") {
  89. Picker("Units",
  90. selection: Binding(
  91. get: { UserDefaultsRepository.units.value },
  92. set: { UserDefaultsRepository.units.value = $0 }
  93. )) {
  94. Text("mg/dL").tag("mg/dL")
  95. Text("mmol/L").tag("mmol/L")
  96. }
  97. .pickerStyle(.segmented)
  98. navRow(title: "Nightscout Settings",
  99. icon: "antenna.radiowaves.left.and.right",
  100. destination: .nightscout)
  101. navRow(title: "Dexcom Settings",
  102. icon: "waveform.path.ecg",
  103. destination: .dexcom)
  104. }
  105. .onAppear {
  106. onNightscoutVisibilityChange(IsNightscoutEnabled())
  107. }
  108. }
  109. /// version / build info
  110. @ViewBuilder
  111. private var buildInfoSection: some View {
  112. let build = BuildDetails.default
  113. let ver = AppVersionManager().version()
  114. Section("Build Information") {
  115. keyValue("Version", ver, tint: versionTint)
  116. keyValue("Latest version", latestVersion ?? "Fetching…")
  117. if !(build.isMacApp() || build.isSimulatorBuild()) {
  118. keyValue(build.expirationHeaderString,
  119. dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
  120. }
  121. keyValue("Built", dateTimeUtils.formattedDate(from: build.buildDate()))
  122. keyValue("Branch", build.branchAndSha)
  123. }
  124. }
  125. // MARK: – Row helpers ----------------------------------------------------
  126. /// Standard row with icon, chevron and sheet presentation
  127. private func navRow(title: String,
  128. icon: String,
  129. destination: Sheet) -> some View
  130. {
  131. NavigationLink {
  132. destination.destination
  133. } label: {
  134. Label(title, systemImage: icon)
  135. }
  136. }
  137. /// Simple key-value row
  138. @ViewBuilder
  139. private func keyValue(_ key: String, _ value: String, tint: Color = .secondary) -> some View {
  140. HStack {
  141. Text(key)
  142. Spacer()
  143. Text(value).foregroundColor(tint)
  144. }
  145. }
  146. // MARK: – Version check --------------------------------------------------
  147. private func refreshVersionInfo() async {
  148. let manager = AppVersionManager()
  149. let (latest, newer, blacklisted) = await manager.checkForNewVersionAsync()
  150. latestVersion = latest ?? "Unknown"
  151. // match old colour logic
  152. let current = manager.version()
  153. versionTint = blacklisted ? .red :
  154. newer ? .orange :
  155. latest == current ? .green : .secondary
  156. }
  157. // MARK: – Share logs -----------------------------------------------------
  158. private func shareLogs() {
  159. let files = LogManager.shared.logFilesForTodayAndYesterday()
  160. guard !files.isEmpty else {
  161. UIApplication.shared.topMost?.presentSimpleAlert(
  162. title: "No Logs Available",
  163. message: "There are no logs to share."
  164. )
  165. return
  166. }
  167. let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
  168. UIApplication.shared.topMost?.present(avc, animated: true)
  169. }
  170. }
  171. // MARK: – Sheet routing identical to earlier -------------------------------
  172. private enum Sheet: Identifiable {
  173. case nightscout, dexcom
  174. case backgroundRefresh
  175. case general, graph
  176. case infoDisplay
  177. case alarmsList, alarmSettings
  178. case remote
  179. case calendar, contact
  180. case advanced
  181. case viewLog
  182. var id: Int { hashValue }
  183. @ViewBuilder
  184. var destination: some View {
  185. switch self {
  186. case .nightscout: NightscoutSettingsView(viewModel: .init())
  187. case .dexcom: DexcomSettingsView(viewModel: .init())
  188. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  189. case .general: GeneralSettingsView()
  190. case .graph: GraphSettingsView()
  191. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  192. case .alarmsList: AlarmListView()
  193. case .alarmSettings: AlarmSettingsView()
  194. case .remote: RemoteSettingsView(viewModel: .init())
  195. case .calendar: WatchSettingsView()
  196. case .contact: ContactSettingsView(viewModel: .init())
  197. case .advanced: AdvancedSettingsView(viewModel: .init())
  198. case .viewLog: LogView(viewModel: .init())
  199. }
  200. }
  201. }
  202. import UIKit
  203. extension UIApplication {
  204. var topMost: UIViewController? {
  205. guard var top = keyWindow?.rootViewController else { return nil }
  206. while let presented = top.presentedViewController {
  207. top = presented
  208. }
  209. return top
  210. }
  211. }
  212. extension UIViewController {
  213. func presentSimpleAlert(title: String, message: String) {
  214. let a = UIAlertController(title: title, message: message, preferredStyle: .alert)
  215. a.addAction(UIAlertAction(title: "OK", style: .default))
  216. present(a, animated: true)
  217. }
  218. }