SettingsMenuView.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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. navRow(title: "Background Refresh Settings",
  22. icon: "arrow.clockwise",
  23. destination: .backgroundRefresh)
  24. navRow(title: "General Settings",
  25. icon: "gearshape",
  26. destination: .general)
  27. navRow(title: "Graph Settings",
  28. icon: "chart.xyaxis.line",
  29. destination: .graph)
  30. if IsNightscoutEnabled() {
  31. navRow(title: "Information Display Settings",
  32. icon: "info.circle",
  33. destination: .infoDisplay)
  34. }
  35. }
  36. // ────────────── Alarms ──────────────
  37. Section {
  38. navRow(title: "Alarms",
  39. icon: "bell",
  40. destination: .alarmsList)
  41. navRow(title: "Alarm Settings",
  42. icon: "bell.badge",
  43. destination: .alarmSettings)
  44. }
  45. // ────────────── Integrations ──────────────
  46. Section("Integrations") {
  47. navRow(title: "Calendar",
  48. icon: "calendar",
  49. destination: .calendar)
  50. navRow(title: "Contact",
  51. icon: "person.circle",
  52. destination: .contact)
  53. }
  54. // ────────────── Advanced / Logs ──────────────
  55. Section("Advanced Settings") {
  56. navRow(title: "Advanced Settings",
  57. icon: "exclamationmark.shield",
  58. destination: .advanced)
  59. }
  60. Section("Logging") {
  61. navRow(title: "View Log",
  62. icon: "doc.text.magnifyingglass",
  63. destination: .viewLog)
  64. actionRow(title: "Share Logs",
  65. icon: "square.and.arrow.up") { shareLogs() }
  66. }
  67. // ────────────── Community ──────────────
  68. Section("Community") {
  69. linkRow(title: "LoopFollow Facebook Group",
  70. icon: "person.2.fill",
  71. url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!)
  72. }
  73. // ────────────── Build info ──────────────
  74. buildInfoSection
  75. }
  76. .navigationTitle("Settings")
  77. }
  78. .task { await refreshVersionInfo() }
  79. .sheet(item: $sheet) { $0.destination }
  80. }
  81. // MARK: – Section builders ----------------------------------------------
  82. /// “Data Settings”
  83. @ViewBuilder
  84. private var dataSection: some View {
  85. Section("Data Settings") {
  86. Picker("Units",
  87. selection: Binding(
  88. get: { UserDefaultsRepository.units.value },
  89. set: { UserDefaultsRepository.units.value = $0 }
  90. )) {
  91. Text("mg/dL").tag("mg/dL")
  92. Text("mmol/L").tag("mmol/L")
  93. }
  94. .pickerStyle(.segmented)
  95. navRow(title: "Nightscout Settings",
  96. icon: "network",
  97. destination: .nightscout)
  98. navRow(title: "Dexcom Settings",
  99. icon: "sensor.tag.radiowaves.forward",
  100. destination: .dexcom)
  101. }
  102. .onAppear {
  103. onNightscoutVisibilityChange(IsNightscoutEnabled())
  104. }
  105. }
  106. /// version / build info
  107. @ViewBuilder
  108. private var buildInfoSection: some View {
  109. let build = BuildDetails.default
  110. let ver = AppVersionManager().version()
  111. Section("Build Information") {
  112. keyValue("Version", ver, tint: versionTint)
  113. keyValue("Latest version", latestVersion ?? "Fetching…")
  114. if !(build.isMacApp() || build.isSimulatorBuild()) {
  115. keyValue(build.expirationHeaderString,
  116. dateTimeUtils.formattedDate(from: build.calculateExpirationDate()))
  117. }
  118. keyValue("Built", dateTimeUtils.formattedDate(from: build.buildDate()))
  119. keyValue("Branch", build.branchAndSha)
  120. }
  121. }
  122. // MARK: – Row helpers ----------------------------------------------------
  123. /// Standard row with icon, chevron and sheet presentation
  124. /// One tappable row, styled like the iOS Settings app
  125. @ViewBuilder
  126. private func navRow(
  127. title: String,
  128. icon: String,
  129. tint: Color = .primary,
  130. destination: Sheet
  131. ) -> some View {
  132. Button {
  133. sheet = destination
  134. } label: {
  135. HStack {
  136. Glyph(symbol: icon, tint: tint)
  137. Text(title)
  138. Spacer()
  139. Image(systemName: "chevron.right")
  140. .foregroundColor(Color(uiColor: .tertiaryLabel))
  141. }
  142. .contentShape(Rectangle())
  143. }
  144. .buttonStyle(.plain)
  145. }
  146. /// Simple key-value row
  147. @ViewBuilder
  148. private func keyValue(_ key: String, _ value: String, tint: Color = .secondary) -> some View {
  149. HStack {
  150. Text(key)
  151. Spacer()
  152. Text(value).foregroundColor(tint)
  153. }
  154. }
  155. // MARK: – Version check --------------------------------------------------
  156. private func refreshVersionInfo() async {
  157. let manager = AppVersionManager()
  158. let (latest, newer, blacklisted) = await manager.checkForNewVersionAsync()
  159. latestVersion = latest ?? "Unknown"
  160. // match old colour logic
  161. let current = manager.version()
  162. versionTint = blacklisted ? .red :
  163. newer ? .orange :
  164. latest == current ? .green : .secondary
  165. }
  166. // MARK: – Share logs -----------------------------------------------------
  167. private func shareLogs() {
  168. let files = LogManager.shared.logFilesForTodayAndYesterday()
  169. guard !files.isEmpty else {
  170. UIApplication.shared.topMost?.presentSimpleAlert(
  171. title: "No Logs Available",
  172. message: "There are no logs to share."
  173. )
  174. return
  175. }
  176. let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
  177. UIApplication.shared.topMost?.present(avc, animated: true)
  178. }
  179. }
  180. // MARK: – Sheet routing identical to earlier -------------------------------
  181. private enum Sheet: Identifiable {
  182. case nightscout, dexcom
  183. case backgroundRefresh
  184. case general, graph
  185. case infoDisplay
  186. case alarmsList, alarmSettings
  187. case remote
  188. case calendar, contact
  189. case advanced
  190. case viewLog
  191. var id: Int { hashValue }
  192. @ViewBuilder
  193. var destination: some View {
  194. switch self {
  195. case .nightscout: NightscoutSettingsView(viewModel: .init())
  196. case .dexcom: DexcomSettingsView(viewModel: .init())
  197. case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init())
  198. case .general: GeneralSettingsView()
  199. case .graph: GraphSettingsView()
  200. case .infoDisplay: InfoDisplaySettingsView(viewModel: .init())
  201. case .alarmsList: AlarmListView()
  202. case .alarmSettings: AlarmSettingsView()
  203. case .remote: RemoteSettingsView(viewModel: .init())
  204. case .calendar: WatchSettingsView()
  205. case .contact: ContactSettingsView(viewModel: .init())
  206. case .advanced: AdvancedSettingsView(viewModel: .init())
  207. case .viewLog: LogView(viewModel: .init())
  208. }
  209. }
  210. }
  211. import UIKit
  212. extension UIApplication {
  213. var topMost: UIViewController? {
  214. guard var top = keyWindow?.rootViewController else { return nil }
  215. while let presented = top.presentedViewController {
  216. top = presented
  217. }
  218. return top
  219. }
  220. }
  221. extension UIViewController {
  222. func presentSimpleAlert(title: String, message: String) {
  223. let a = UIAlertController(title: title, message: message, preferredStyle: .alert)
  224. a.addAction(UIAlertAction(title: "OK", style: .default))
  225. present(a, animated: true)
  226. }
  227. }
  228. struct Glyph: View {
  229. let symbol: String
  230. let tint: Color
  231. @Environment(\.colorScheme) private var scheme
  232. var body: some View {
  233. ZStack {
  234. RoundedRectangle(cornerRadius: 8, style: .continuous)
  235. .fill(Color(uiColor: .systemGray))
  236. .frame(width: 28, height: 28)
  237. Image(systemName: symbol)
  238. .font(.system(size: 16, weight: .regular))
  239. .foregroundStyle(tint)
  240. }
  241. .frame(width: 36, height: 36)
  242. }
  243. }
  244. @ViewBuilder
  245. private func actionRow(
  246. title: String,
  247. icon: String,
  248. tint: Color = .primary,
  249. action: @escaping () -> Void
  250. ) -> some View {
  251. Button { action() } label: {
  252. HStack {
  253. Glyph(symbol: icon, tint: tint)
  254. Text(title)
  255. Spacer()
  256. Image(systemName: "chevron.right")
  257. .foregroundColor(Color(uiColor: .tertiaryLabel))
  258. }
  259. .contentShape(Rectangle())
  260. }
  261. .buttonStyle(.plain)
  262. }
  263. @ViewBuilder
  264. private func linkRow(
  265. title: String,
  266. icon: String,
  267. tint: Color = .primary,
  268. url: URL
  269. ) -> some View {
  270. actionRow(title: title, icon: icon, tint: tint) {
  271. UIApplication.shared.open(url)
  272. }
  273. }