NightscoutSettingsView.swift 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // LoopFollow
  2. // NightscoutSettingsView.swift
  3. import SwiftUI
  4. struct NightscoutSettingsView: View {
  5. @ObservedObject var viewModel: NightscoutSettingsViewModel
  6. var usesModalCloseButton: Bool = false
  7. var onContinueToUnits: (() -> Void)? = nil
  8. var onImportSettings: (() -> Void)? = nil
  9. @State private var showUnitsSetup = false
  10. @Environment(\.dismiss) private var dismiss
  11. var body: some View {
  12. Form {
  13. urlSection
  14. tokenSection
  15. statusSection
  16. webSocketSection
  17. if viewModel.isFreshSetup {
  18. continueSection
  19. }
  20. importSection
  21. }
  22. .navigationDestination(isPresented: $showUnitsSetup) {
  23. UnitsOnboardingView {
  24. dismiss()
  25. }
  26. }
  27. .navigationBarTitle("Nightscout Settings", displayMode: .inline)
  28. .navigationBarBackButtonHidden(usesModalCloseButton)
  29. .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
  30. }
  31. // MARK: - Subviews / Computed Properties
  32. private var urlSection: some View {
  33. Section(header: Text("URL")) {
  34. TextField("Enter URL", text: $viewModel.nightscoutURL)
  35. .textContentType(.username)
  36. .autocapitalization(.none)
  37. .disableAutocorrection(true)
  38. .onChange(of: viewModel.nightscoutURL) { newValue in
  39. viewModel.processURL(newValue)
  40. }
  41. }
  42. }
  43. private var tokenSection: some View {
  44. Section {
  45. HStack {
  46. Text("Access Token")
  47. TogglableSecureInput(
  48. placeholder: "Token or API secret",
  49. text: $viewModel.nightscoutToken,
  50. style: .singleLine,
  51. textContentType: .password
  52. )
  53. }
  54. if viewModel.tokenIsVerifiedSecret || viewModel.isProvisioningToken {
  55. Button {
  56. viewModel.createReadOnlyToken(fromSecret: viewModel.nightscoutToken)
  57. } label: {
  58. HStack {
  59. if viewModel.isProvisioningToken {
  60. ProgressView()
  61. Text("Creating read-only token…")
  62. } else {
  63. Image(systemName: "wand.and.stars")
  64. Text("That's your API secret — create a read-only token")
  65. }
  66. }
  67. }
  68. .disabled(viewModel.isProvisioningToken)
  69. }
  70. if let error = viewModel.tokenProvisionError {
  71. Text(error)
  72. .font(.footnote)
  73. .foregroundColor(.red)
  74. }
  75. } header: {
  76. Text("Token")
  77. } footer: {
  78. Text("Paste a Nightscout token. If your site needs one and you only have the API secret, paste that instead — LoopFollow can create a read-only token for you.")
  79. }
  80. }
  81. private var statusSection: some View {
  82. Section(header: Text("Status")) {
  83. HStack {
  84. Text(viewModel.nightscoutStatus)
  85. if viewModel.isConnected {
  86. Spacer()
  87. Image(systemName: "checkmark.circle.fill")
  88. .foregroundColor(.green)
  89. }
  90. }
  91. }
  92. }
  93. private var continueSection: some View {
  94. Section {
  95. Button(action: {
  96. if let onContinueToUnits {
  97. onContinueToUnits()
  98. } else {
  99. showUnitsSetup = true
  100. }
  101. }) {
  102. HStack {
  103. Spacer()
  104. Text("Continue")
  105. .fontWeight(.semibold)
  106. Spacer()
  107. }
  108. }
  109. .buttonStyle(.borderedProminent)
  110. .disabled(!viewModel.isConnected)
  111. .listRowBackground(Color.clear)
  112. }
  113. }
  114. @State private var showWebSocketInfo = false
  115. private var webSocketSection: some View {
  116. Section(header: webSocketSectionHeader) {
  117. Toggle("Enable WebSocket", isOn: $viewModel.webSocketEnabled)
  118. if viewModel.webSocketEnabled {
  119. HStack {
  120. Text("Status")
  121. Spacer()
  122. Text(viewModel.webSocketStatus)
  123. .foregroundColor(viewModel.webSocketStatusColor)
  124. }
  125. }
  126. }
  127. .sheet(isPresented: $showWebSocketInfo) {
  128. NavigationStack {
  129. ScrollView {
  130. Text("""
  131. When enabled, LoopFollow maintains a live connection to your Nightscout server using WebSocket while the app is in the foreground. Data updates (new glucose readings, treatments, device status) arrive within seconds instead of waiting for the next polling cycle.
  132. The WebSocket disconnects when LoopFollow moves to the background and reconnects when you return to the app. Polling continues to handle updates while the app is in the background.
  133. In the foreground, polling continues at a reduced frequency as a safety net. If the WebSocket connection drops, normal polling resumes immediately.
  134. """)
  135. .padding()
  136. .frame(maxWidth: .infinity, alignment: .leading)
  137. }
  138. .navigationTitle("Real-time Updates")
  139. .navigationBarTitleDisplayMode(.inline)
  140. .toolbar {
  141. ToolbarItem(placement: .confirmationAction) {
  142. Button("Done") { showWebSocketInfo = false }
  143. }
  144. }
  145. }
  146. .presentationDetents([.medium])
  147. .presentationDragIndicator(.visible)
  148. }
  149. }
  150. private var webSocketSectionHeader: some View {
  151. HStack(spacing: 4) {
  152. Text("Real-time Updates")
  153. Button {
  154. showWebSocketInfo = true
  155. } label: {
  156. Image(systemName: "info.circle")
  157. .foregroundStyle(Color.accentColor)
  158. }
  159. .buttonStyle(.plain)
  160. }
  161. }
  162. private var importSection: some View {
  163. Section(header: Text("Import Settings")) {
  164. if let onImportSettings {
  165. Button(action: onImportSettings) {
  166. HStack {
  167. Image(systemName: "square.and.arrow.down")
  168. .foregroundColor(.blue)
  169. Text("Import Settings from QR Code")
  170. .foregroundColor(.primary)
  171. }
  172. }
  173. } else {
  174. NavigationLink(destination: ImportExportSettingsView()) {
  175. HStack {
  176. Image(systemName: "square.and.arrow.down")
  177. .foregroundColor(.blue)
  178. Text("Import Settings from QR Code")
  179. }
  180. }
  181. }
  182. }
  183. }
  184. }