GraphSettingsView.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. // LoopFollow
  2. // GraphSettingsView.swift
  3. import SwiftUI
  4. struct GraphSettingsView: View {
  5. @ObservedObject private var showDots = Storage.shared.showDots
  6. @ObservedObject private var showLines = Storage.shared.showLines
  7. @ObservedObject private var showValues = Storage.shared.showValues
  8. @ObservedObject private var showAbsorption = Storage.shared.showAbsorption
  9. @ObservedObject private var showDIALines = Storage.shared.showDIALines
  10. @ObservedObject private var show30MinLine = Storage.shared.show30MinLine
  11. @ObservedObject private var show90MinLine = Storage.shared.show90MinLine
  12. @ObservedObject private var showMidnightLines = Storage.shared.showMidnightLines
  13. @ObservedObject private var smallGraphTreatments = Storage.shared.smallGraphTreatments
  14. @ObservedObject private var graphTimeZoneEnabled = Storage.shared.graphTimeZoneEnabled
  15. @ObservedObject private var graphTimeZoneIdentifier = Storage.shared.graphTimeZoneIdentifier
  16. @ObservedObject private var smallGraphHeight = Storage.shared.smallGraphHeight
  17. @ObservedObject private var predictionToLoad = Storage.shared.predictionToLoad
  18. @ObservedObject private var minBasalScale = Storage.shared.minBasalScale
  19. @ObservedObject private var minBGScale = Storage.shared.minBGScale
  20. @ObservedObject private var lowLine = Storage.shared.lowLine
  21. @ObservedObject private var highLine = Storage.shared.highLine
  22. @ObservedObject private var downloadDays = Storage.shared.downloadDays
  23. private var nightscoutEnabled: Bool { IsNightscoutEnabled() }
  24. var body: some View {
  25. NavigationView {
  26. Form {
  27. // ── Graph Display ────────────────────────────────────────────
  28. Section("Graph Display") {
  29. Toggle("Display Dots", isOn: $showDots.value)
  30. .onChange(of: showDots.value) { _ in markDirty() }
  31. Toggle("Display Lines", isOn: $showLines.value)
  32. .onChange(of: showLines.value) { _ in markDirty() }
  33. if nightscoutEnabled {
  34. Toggle("Show DIA Lines", isOn: $showDIALines.value)
  35. .onChange(of: showDIALines.value) { _ in markDirty() }
  36. Toggle("Show −30 min Line", isOn: $show30MinLine.value)
  37. .onChange(of: show30MinLine.value) { _ in markDirty() }
  38. Toggle("Show −90 min Line", isOn: $show90MinLine.value)
  39. .onChange(of: show90MinLine.value) { _ in markDirty() }
  40. }
  41. Toggle("Show Midnight Lines", isOn: $showMidnightLines.value)
  42. .onChange(of: showMidnightLines.value) { _ in markDirty() }
  43. Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value)
  44. .onChange(of: graphTimeZoneEnabled.value) { _ in markDirty() }
  45. if graphTimeZoneEnabled.value {
  46. Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) {
  47. ForEach(Self.sortedTimeZones, id: \.identifier) { tz in
  48. Text(Self.timeZoneLabel(tz)).tag(tz.identifier)
  49. }
  50. }
  51. .onChange(of: graphTimeZoneIdentifier.value) { _ in markDirty() }
  52. }
  53. }
  54. // ── Treatments ───────────────────────────────────────────────
  55. if nightscoutEnabled {
  56. Section("Treatments") {
  57. Toggle("Show Carb/Bolus Values", isOn: $showValues.value)
  58. Toggle("Show Carb Absorption", isOn: $showAbsorption.value)
  59. Toggle("Treatments on Small Graph",
  60. isOn: $smallGraphTreatments.value)
  61. }
  62. }
  63. // ── Small Graph ──────────────────────────────────────────────
  64. Section("Small Graph") {
  65. SettingsStepperRow(
  66. title: "Height",
  67. range: 40 ... 80,
  68. step: 5,
  69. value: $smallGraphHeight.value,
  70. format: { "\(Int($0)) pt" }
  71. )
  72. .onChange(of: smallGraphHeight.value) { _ in markDirty() }
  73. }
  74. // ── Prediction ───────────────────────────────────────────────
  75. if nightscoutEnabled {
  76. Section("Prediction") {
  77. SettingsStepperRow(
  78. title: "Hours of Prediction",
  79. range: 0 ... 6,
  80. step: 0.25,
  81. value: $predictionToLoad.value,
  82. format: { "\($0.localized(maxFractionDigits: 2)) h" }
  83. )
  84. }
  85. }
  86. // ── Basal / BG scale ─────────────────────────────────────────
  87. if nightscoutEnabled {
  88. Section("Basal / BG Scale") {
  89. SettingsStepperRow(
  90. title: "Min Basal",
  91. range: 0.5 ... 20,
  92. step: 0.5,
  93. value: $minBasalScale.value,
  94. format: { "\($0.localized(maxFractionDigits: 1)) U/h" }
  95. )
  96. BGPicker(
  97. title: "Min BG Scale",
  98. range: 40 ... 400,
  99. value: $minBGScale.value
  100. )
  101. .onChange(of: minBGScale.value) { _ in markDirty() }
  102. }
  103. }
  104. // ── Target lines ─────────────────────────────────────────────
  105. Section("Target Lines") {
  106. BGPicker(title: "Low BG Line",
  107. range: 40 ... 120,
  108. value: $lowLine.value)
  109. .onChange(of: lowLine.value) { _ in markDirty() }
  110. BGPicker(title: "High BG Line",
  111. range: 120 ... 400,
  112. value: $highLine.value)
  113. .onChange(of: highLine.value) { _ in markDirty() }
  114. }
  115. // ── History window ───────────────────────────────────────────
  116. if nightscoutEnabled {
  117. Section("History") {
  118. SettingsStepperRow(
  119. title: "Show Days Back",
  120. range: 1 ... 4,
  121. step: 1,
  122. value: $downloadDays.value,
  123. format: { "\(Int($0)) d" }
  124. )
  125. }
  126. }
  127. }
  128. }
  129. .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
  130. .navigationBarTitle("Graph Settings", displayMode: .inline)
  131. }
  132. /// Marks the chart as needing a redraw
  133. private func markDirty() {
  134. Observable.shared.chartSettingsChanged.value = true
  135. }
  136. // MARK: - Time Zone Helpers
  137. private static let sortedTimeZones: [TimeZone] = TimeZone.knownTimeZoneIdentifiers
  138. .compactMap { TimeZone(identifier: $0) }
  139. .sorted { $0.secondsFromGMT() < $1.secondsFromGMT() }
  140. private static func timeZoneLabel(_ tz: TimeZone) -> String {
  141. let offsetMinutes = tz.secondsFromGMT() / 60
  142. let sign = offsetMinutes >= 0 ? "+" : "-"
  143. let offsetString = String(format: "UTC%@%02d:%02d", sign, abs(offsetMinutes) / 60, abs(offsetMinutes) % 60)
  144. return "(\(offsetString)) \(tz.identifier)"
  145. }
  146. }