| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- // LoopFollow
- // NightscoutSettingsView.swift
- import SwiftUI
- struct NightscoutSettingsView: View {
- @ObservedObject var viewModel: NightscoutSettingsViewModel
- var usesModalCloseButton: Bool = false
- var onContinueToUnits: (() -> Void)? = nil
- var onImportSettings: (() -> Void)? = nil
- @State private var showUnitsSetup = false
- @Environment(\.dismiss) private var dismiss
- var body: some View {
- Form {
- urlSection
- tokenSection
- statusSection
- webSocketSection
- if viewModel.isFreshSetup {
- continueSection
- }
- importSection
- }
- .navigationDestination(isPresented: $showUnitsSetup) {
- UnitsOnboardingView {
- dismiss()
- }
- }
- .navigationBarTitle("Nightscout Settings", displayMode: .inline)
- .navigationBarBackButtonHidden(usesModalCloseButton)
- .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
- }
- // MARK: - Subviews / Computed Properties
- private var urlSection: some View {
- Section(header: Text("URL")) {
- TextField("Enter URL", text: $viewModel.nightscoutURL)
- .textContentType(.username)
- .autocapitalization(.none)
- .disableAutocorrection(true)
- .onChange(of: viewModel.nightscoutURL) { newValue in
- viewModel.processURL(newValue)
- }
- }
- }
- private var tokenSection: some View {
- Section {
- HStack {
- Text("Access Token")
- TogglableSecureInput(
- placeholder: "Token or API secret",
- text: $viewModel.nightscoutToken,
- style: .singleLine,
- textContentType: .password
- )
- }
- if viewModel.tokenIsVerifiedSecret || viewModel.isProvisioningToken {
- Button {
- viewModel.createReadOnlyToken(fromSecret: viewModel.nightscoutToken)
- } label: {
- HStack {
- if viewModel.isProvisioningToken {
- ProgressView()
- Text("Creating read-only token…")
- } else {
- Image(systemName: "wand.and.stars")
- Text("That's your API secret — create a read-only token")
- }
- }
- }
- .disabled(viewModel.isProvisioningToken)
- }
- if let error = viewModel.tokenProvisionError {
- Text(error)
- .font(.footnote)
- .foregroundColor(.red)
- }
- } header: {
- Text("Token")
- } footer: {
- 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.")
- }
- }
- private var statusSection: some View {
- Section(header: Text("Status")) {
- HStack {
- Text(viewModel.nightscoutStatus)
- if viewModel.isConnected {
- Spacer()
- Image(systemName: "checkmark.circle.fill")
- .foregroundColor(.green)
- }
- }
- }
- }
- private var continueSection: some View {
- Section {
- Button(action: {
- if let onContinueToUnits {
- onContinueToUnits()
- } else {
- showUnitsSetup = true
- }
- }) {
- HStack {
- Spacer()
- Text("Continue")
- .fontWeight(.semibold)
- Spacer()
- }
- }
- .buttonStyle(.borderedProminent)
- .disabled(!viewModel.isConnected)
- .listRowBackground(Color.clear)
- }
- }
- @State private var showWebSocketInfo = false
- private var webSocketSection: some View {
- Section(header: webSocketSectionHeader) {
- Toggle("Enable WebSocket", isOn: $viewModel.webSocketEnabled)
- if viewModel.webSocketEnabled {
- HStack {
- Text("Status")
- Spacer()
- Text(viewModel.webSocketStatus)
- .foregroundColor(viewModel.webSocketStatusColor)
- }
- }
- }
- .sheet(isPresented: $showWebSocketInfo) {
- NavigationStack {
- ScrollView {
- Text("""
- 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.
- 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.
- In the foreground, polling continues at a reduced frequency as a safety net. If the WebSocket connection drops, normal polling resumes immediately.
- """)
- .padding()
- .frame(maxWidth: .infinity, alignment: .leading)
- }
- .navigationTitle("Real-time Updates")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .confirmationAction) {
- Button("Done") { showWebSocketInfo = false }
- }
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
- }
- private var webSocketSectionHeader: some View {
- HStack(spacing: 4) {
- Text("Real-time Updates")
- Button {
- showWebSocketInfo = true
- } label: {
- Image(systemName: "info.circle")
- .foregroundStyle(Color.accentColor)
- }
- .buttonStyle(.plain)
- }
- }
- private var importSection: some View {
- Section(header: Text("Import Settings")) {
- if let onImportSettings {
- Button(action: onImportSettings) {
- HStack {
- Image(systemName: "square.and.arrow.down")
- .foregroundColor(.blue)
- Text("Import Settings from QR Code")
- .foregroundColor(.primary)
- }
- }
- } else {
- NavigationLink(destination: ImportExportSettingsView()) {
- HStack {
- Image(systemName: "square.and.arrow.down")
- .foregroundColor(.blue)
- Text("Import Settings from QR Code")
- }
- }
- }
- }
- }
- }
|