SettingsMenuView.swift 8.7 KB

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