GeneralSettingsView.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. // LoopFollow
  2. // GeneralSettingsView.swift
  3. import SwiftUI
  4. struct GeneralSettingsView: View {
  5. @ObservedObject var colorBGText = Storage.shared.colorBGText
  6. @ObservedObject var appBadge = Storage.shared.appBadge
  7. @ObservedObject var appearanceMode = Storage.shared.appearanceMode
  8. @ObservedObject var appLanguage = Storage.shared.appLanguage
  9. @State private var showLanguageRestartAlert = false
  10. @ObservedObject var showStats = Storage.shared.showStats
  11. @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph
  12. @ObservedObject var screenlockSwitchState = Storage.shared.screenlockSwitchState
  13. @ObservedObject var showDisplayName = Storage.shared.showDisplayName
  14. @ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji
  15. @ObservedObject var forcePortraitMode = Storage.shared.forcePortraitMode
  16. @ObservedObject var persistentNotification = Storage.shared.persistentNotification
  17. @ObservedObject var graphTimeZoneEnabled = Storage.shared.graphTimeZoneEnabled
  18. @ObservedObject var graphTimeZoneIdentifier = Storage.shared.graphTimeZoneIdentifier
  19. // Speak-BG settings
  20. @ObservedObject var speakBG = Storage.shared.speakBG
  21. @ObservedObject var speakBGAlways = Storage.shared.speakBGAlways
  22. @ObservedObject var speakLanguage = Storage.shared.speakLanguage
  23. @ObservedObject var speakLowBG = Storage.shared.speakLowBG
  24. @ObservedObject var speakProactiveLowBG = Storage.shared.speakProactiveLowBG
  25. @ObservedObject var speakLowBGLimit = Storage.shared.speakLowBGLimit
  26. @ObservedObject var speakFastDropDelta = Storage.shared.speakFastDropDelta
  27. @ObservedObject var speakHighBG = Storage.shared.speakHighBG
  28. @ObservedObject var speakHighBGLimit = Storage.shared.speakHighBGLimit
  29. // Telemetry — see LoopFollow/Helpers/Telemetry.swift
  30. @ObservedObject var telemetryEnabled = Storage.shared.telemetryEnabled
  31. var body: some View {
  32. Form {
  33. Section("App Settings") {
  34. Toggle("Display App Badge", isOn: $appBadge.value)
  35. Toggle("Persistent Notification", isOn: $persistentNotification.value)
  36. }
  37. Section {
  38. Picker("App Language", selection: $appLanguage.value) {
  39. ForEach(AppLanguage.allCases, id: \.self) { lang in
  40. Text(lang.displayName).tag(lang)
  41. }
  42. }
  43. .onChange(of: appLanguage.value) { newValue in
  44. newValue.applyToAppleLanguages()
  45. showLanguageRestartAlert = true
  46. }
  47. } header: {
  48. Text("Language")
  49. } footer: {
  50. Text("Overrides the system language for this app. Restart the app to apply.")
  51. }
  52. Section("Display") {
  53. Picker("Appearance", selection: $appearanceMode.value) {
  54. ForEach(AppearanceMode.allCases, id: \.self) { mode in
  55. Text(mode.displayName).tag(mode)
  56. }
  57. }
  58. Toggle("Display Stats", isOn: $showStats.value)
  59. Toggle("Display Small Graph", isOn: $showSmallGraph.value)
  60. Toggle("Color BG Text", isOn: $colorBGText.value)
  61. Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value)
  62. Toggle("Show Display Name", isOn: $showDisplayName.value)
  63. Toggle("Snoozer emoji", isOn: $snoozerEmoji.value)
  64. Toggle("Force portrait mode", isOn: $forcePortraitMode.value)
  65. .onChange(of: forcePortraitMode.value) { _ in
  66. let window = UIApplication.shared.connectedScenes
  67. .compactMap { $0 as? UIWindowScene }
  68. .flatMap { $0.windows }
  69. .first
  70. window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
  71. }
  72. }
  73. Section("Time Zone") {
  74. Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value)
  75. .onChange(of: graphTimeZoneEnabled.value) { _ in markChartSettingsDirty() }
  76. if graphTimeZoneEnabled.value {
  77. Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) {
  78. ForEach(Self.sortedTimeZones, id: \.identifier) { tz in
  79. Text(Self.timeZoneLabel(tz)).tag(tz.identifier)
  80. }
  81. }
  82. .onChange(of: graphTimeZoneIdentifier.value) { _ in markChartSettingsDirty() }
  83. }
  84. }
  85. Section("Speak BG") {
  86. Toggle("Speak BG", isOn: $speakBG.value.animation())
  87. if speakBG.value {
  88. Picker("Language", selection: $speakLanguage.value) {
  89. Text("English").tag("en")
  90. Text("French").tag("fr")
  91. Text("Italian").tag("it")
  92. Text("Slovak").tag("sk")
  93. Text("Swedish").tag("sv")
  94. }
  95. Toggle("Always", isOn: $speakBGAlways.value.animation())
  96. if !speakBGAlways.value {
  97. Toggle("Low", isOn: $speakLowBG.value.animation())
  98. .onChange(of: speakLowBG.value) { newValue in
  99. if newValue {
  100. speakProactiveLowBG.value = false
  101. }
  102. }
  103. Toggle("Proactive Low", isOn: $speakProactiveLowBG.value.animation())
  104. .onChange(of: speakProactiveLowBG.value) { newValue in
  105. if newValue {
  106. speakLowBG.value = false
  107. }
  108. }
  109. if speakLowBG.value || speakProactiveLowBG.value {
  110. BGPicker(
  111. title: String(localized: "Low BG Limit"),
  112. range: 40 ... 108,
  113. value: $speakLowBGLimit.value
  114. )
  115. }
  116. if speakProactiveLowBG.value {
  117. BGPicker(
  118. title: String(localized: "Fast Drop Delta"),
  119. range: 3 ... 20,
  120. value: $speakFastDropDelta.value
  121. )
  122. }
  123. Toggle("High", isOn: $speakHighBG.value.animation())
  124. if speakHighBG.value {
  125. BGPicker(
  126. title: String(localized: "High BG Limit"),
  127. range: 140 ... 300,
  128. value: $speakHighBGLimit.value
  129. )
  130. }
  131. }
  132. }
  133. }
  134. Section("Diagnostics") {
  135. Toggle("Send anonymous usage stats", isOn: $telemetryEnabled.value)
  136. .onChange(of: telemetryEnabled.value) { newValue in
  137. if newValue {
  138. TelemetryClient.shared.scheduleRecurring()
  139. }
  140. }
  141. NavigationLink("What's sent") { TelemetryPreviewView() }
  142. NavigationLink("Privacy") { TelemetryPrivacyView() }
  143. }
  144. }
  145. .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
  146. .navigationBarTitle("General Settings", displayMode: .inline)
  147. .alert("Restart Required", isPresented: $showLanguageRestartAlert) {
  148. Button("OK", role: .cancel) {}
  149. } message: {
  150. Text("Fully close and reopen LoopFollow for the language change to take effect.")
  151. }
  152. }
  153. private func markChartSettingsDirty() {
  154. Observable.shared.chartSettingsChanged.value = true
  155. }
  156. private static let sortedTimeZones: [TimeZone] = TimeZone.knownTimeZoneIdentifiers
  157. .compactMap { TimeZone(identifier: $0) }
  158. .sorted { $0.secondsFromGMT() < $1.secondsFromGMT() }
  159. private static func timeZoneLabel(_ tz: TimeZone) -> String {
  160. let offsetMinutes = tz.secondsFromGMT() / 60
  161. let sign = offsetMinutes >= 0 ? "+" : "-"
  162. let offsetString = String(format: "UTC%@%02d:%02d", sign, abs(offsetMinutes) / 60, abs(offsetMinutes) % 60)
  163. return "(\(offsetString)) \(tz.identifier)"
  164. }
  165. }