Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct BackgroundRefreshSettingsView: View {
@ObservedObject var viewModel: BackgroundRefreshSettingsViewModel
@State private var forceRefresh = false
@State private var timer: Timer?
@State private var showInfo = false

@ObservedObject var bleManager = BLEManager.shared

Expand All @@ -20,6 +21,9 @@ struct BackgroundRefreshSettingsView: View {
availableDevicesSection
}
}
.sheet(isPresented: $showInfo) {
backgroundRefreshInfoSheet
}
.onAppear {
startTimer()
}
Expand All @@ -34,7 +38,7 @@ struct BackgroundRefreshSettingsView: View {
// MARK: - Subviews / Computed Properties

private var refreshTypeSection: some View {
Section {
Section(header: refreshTypeSectionHeader) {
Picker("Background Refresh Type", selection: $viewModel.backgroundRefreshType) {
ForEach(BackgroundRefreshType.allCases, id: \.self) { type in
Text(type.rawValue).tag(type)
Expand Down Expand Up @@ -176,6 +180,64 @@ struct BackgroundRefreshSettingsView: View {
}
}

// MARK: - Section Header & Info Sheet

private var refreshTypeSectionHeader: some View {
HStack(spacing: 4) {
Text("Background Refresh")
Button {
showInfo = true
} label: {
Image(systemName: "info.circle")
.foregroundStyle(Color.accentColor)
}
.buttonStyle(.plain)
}
}

private var backgroundRefreshInfoSheet: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("LoopFollow needs to stay active in the background to check for alarms and update glucose values. There are several methods available:")

Text("Silent Tune")
.font(.headline)
Text("Plays a silent audio track to keep the app active. This has several drawbacks including battery drain and limited reliability — it may be interrupted by other apps.")

Text("Bluetooth Heartbeat")
.font(.headline)
Text("Uses an external Bluetooth device to keep LoopFollow awake. This can save significantly on battery and provides more reliable background operation.")

Text("Supported Bluetooth Devices")
.font(.headline)
Text(verbatim: """
• Radiolink: RileyLink, OrangeLink, Emalink — heartbeat every minute
• Dexcom G5/G6/ONE/Anubis transmitter — heartbeat every ~5 minutes
• Dexcom G7/ONE+ sensor — heartbeat every ~5 minutes

Dexcom device batteries continue to provide Bluetooth power for months after they are no longer in service with a sensor.
""")

Text("If the person using LoopFollow is also wearing a Dexcom or radiolink, they should choose their own device.")
.font(.footnote)
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle("Background Refresh")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { showInfo = false }
}
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}

private func startTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.forceRefresh.toggle()
Expand Down
103 changes: 100 additions & 3 deletions LoopFollow/Nightscout/NightscoutSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import SwiftUI

struct NightscoutSettingsView: View {
@ObservedObject var viewModel: NightscoutSettingsViewModel
@State private var activeInfoSheet: InfoSheet?

private enum InfoSheet: Identifiable {
case url, token
var id: Self { self }
}

var body: some View {
NavigationView {
Expand All @@ -14,6 +20,12 @@ struct NightscoutSettingsView: View {
statusSection
importSection
}
.sheet(item: $activeInfoSheet) { sheet in
switch sheet {
case .url: urlInfoSheet
case .token: tokenInfoSheet
}
}
.onDisappear {
viewModel.dismiss()
}
Expand All @@ -22,10 +34,10 @@ struct NightscoutSettingsView: View {
.navigationBarTitle("Nightscout Settings", displayMode: .inline)
}

// MARK: - Subviews / Computed Properties
// MARK: - Sections

private var urlSection: some View {
Section(header: Text("URL")) {
Section(header: sectionHeader("URL", sheet: .url)) {
TextField("Enter URL", text: $viewModel.nightscoutURL)
.textContentType(.username)
.autocapitalization(.none)
Expand All @@ -37,7 +49,7 @@ struct NightscoutSettingsView: View {
}

private var tokenSection: some View {
Section(header: Text("Token")) {
Section(header: sectionHeader("Token", sheet: .token)) {
HStack {
Text("Access Token")
TogglableSecureInput(
Expand Down Expand Up @@ -67,4 +79,89 @@ struct NightscoutSettingsView: View {
}
}
}

// MARK: - Section Header

private func sectionHeader(_ title: String, sheet: InfoSheet) -> some View {
HStack(spacing: 4) {
Text(title)
Button {
activeInfoSheet = sheet
} label: {
Image(systemName: "info.circle")
.foregroundStyle(Color.accentColor)
}
.buttonStyle(.plain)
}
}

// MARK: - Info Sheets

private var urlInfoSheet: some View {
NavigationStack {
ScrollView {
Text(verbatim: """
Enter your Nightscout site URL, for example:
https://yoursite.yourprovider.com

You can copy your full URL (including the token) from Nightscout Admin Tools. When pasted here, LoopFollow automatically extracts both the URL and the token.

To find your URL, open your Nightscout site in a browser and copy the address from the address bar. Remove any trailing slashes or path components — just the base URL is needed.
""")
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle("Nightscout URL")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { activeInfoSheet = nil }
}
}
}
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}

private var tokenInfoSheet: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 16) {
Text("""
A token controls what LoopFollow can access on your Nightscout site. Tokens are not the same as API keys — they are created within Nightscout itself.
""")

Text("Creating a Token")
.font(.headline)
Text("""
1. Open your Nightscout site in a browser
2. Go to the hamburger menu (☰) and select Admin Tools
3. Under "Subjects", tap "Add new Subject"
4. Enter a name (e.g. "LoopFollow") and select a role
5. Save, then copy the token (it looks like: loopfollow-1234567890abcdef)
""")

Text("Which Role Do I Need?")
.font(.headline)
Text("""
• Read — sufficient for most setups, including Loop and Trio remote control via APNS
• Read & Write (Careportal) — required only for Nightscout Remote Control (Trio 0.2.x or older)

If your Nightscout site is publicly readable, you can leave the token empty. The status will show "OK (Read)".
""")
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
}
.navigationTitle("Access Token")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { activeInfoSheet = nil }
}
}
}
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
}
Loading