SettingsMenuView.swift 10 KB

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