From b1f91f31276d2deca84004eac62584bf77c7f589 Mon Sep 17 00:00:00 2001 From: codebymini Date: Sat, 23 Aug 2025 00:09:19 +0200 Subject: [PATCH 1/4] Add modal for selecting duration of overrides for Loop --- .../Remote/LoopAPNS/OverridePresetsView.swift | 215 ++++++++++++++++-- 1 file changed, 195 insertions(+), 20 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index d34d8fffb..de919629d 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -67,8 +67,7 @@ struct OverridePresetsView: View { isActivating: viewModel.isActivating && viewModel.selectedPreset?.name == preset.name, onActivate: { viewModel.selectedPreset = preset - viewModel.alertType = .confirmActivation - viewModel.showAlert = true + viewModel.showOverrideModal = true } ) } @@ -87,21 +86,24 @@ struct OverridePresetsView: View { await viewModel.loadOverridePresets() } } - .alert(isPresented: $viewModel.showAlert) { - switch viewModel.alertType { - case .confirmActivation: - return Alert( - title: Text("Activate Override"), - message: Text("Do you want to activate the override '\(viewModel.selectedPreset?.name ?? "")'?"), - primaryButton: .default(Text("Confirm"), action: { - if let preset = viewModel.selectedPreset { - Task { - await viewModel.activateOverride(preset: preset) - } + .sheet(isPresented: $viewModel.showOverrideModal) { + if let preset = viewModel.selectedPreset { + OverrideActivationModal( + preset: preset, + onActivate: { duration in + viewModel.showOverrideModal = false + Task { + await viewModel.activateOverride(preset: preset, duration: duration) } - }), - secondaryButton: .cancel() + }, + onCancel: { + viewModel.showOverrideModal = false + } ) + } + } + .alert(isPresented: $viewModel.showAlert) { + switch viewModel.alertType { case .confirmCancellation: return Alert( title: Text("Cancel Override"), @@ -191,6 +193,179 @@ struct OverridePresetRow: View { } } +struct OverrideActivationModal: View { + let preset: OverridePreset + let onActivate: (TimeInterval?) -> Void + let onCancel: () -> Void + + @State private var enableIndefinitely: Bool + @State private var durationHours: Double = 1.0 + + init(preset: OverridePreset, onActivate: @escaping (TimeInterval?) -> Void, onCancel: @escaping () -> Void) { + self.preset = preset + self.onActivate = onActivate + self.onCancel = onCancel + + // Initialize state based on preset duration + if preset.duration == 0 { + // Indefinite override + _enableIndefinitely = State(initialValue: true) + } else { + // Override with default duration + _enableIndefinitely = State(initialValue: false) + _durationHours = State(initialValue: preset.duration / 3600) + } + } + + var body: some View { + NavigationView { + VStack(spacing: 20) { + // Preset Info + VStack(spacing: 12) { + if let symbol = preset.symbol { + Text(symbol) + .font(.largeTitle) + } + + Text(preset.name) + .font(.title2) + .fontWeight(.semibold) + .multilineTextAlignment(.center) + + if let targetRange = preset.targetRange { + Text("Target: \(Int(targetRange.lowerBound))-\(Int(targetRange.upperBound))") + .font(.subheadline) + .foregroundColor(.secondary) + } + + if let insulinNeedsScaleFactor = preset.insulinNeedsScaleFactor { + Text("Insulin: \(Int(insulinNeedsScaleFactor * 100))%") + .font(.subheadline) + .foregroundColor(.secondary) + } + } + .padding(.top) + + Spacer() + + // Duration Settings (moved to bottom for easier access) + VStack(spacing: 16) { + // Warning for overrides with default durations + if preset.duration != 0 { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("Overrides with default durations can't be set to indefinite") + .font(.caption) + .foregroundColor(.orange) + } + .padding(.horizontal) + } + + // Duration Input (shown when not indefinite or when override has default duration) + if !enableIndefinitely || preset.duration != 0 { + VStack(spacing: 8) { + HStack { + Text("Duration") + .font(.headline) + Spacer() + Text(formatDuration(durationHours)) + .font(.headline) + .foregroundColor(preset.duration != 0 ? .secondary : .blue) + } + + Slider(value: $durationHours, in: 0.25 ... max(24.0, preset.duration / 3600), step: 0.25) + .accentColor(.blue) + .disabled(preset.duration != 0) // Disable slider for overrides with default durations + + if preset.duration != 0 { + Text("Using preset's default duration") + .font(.caption) + .foregroundColor(.secondary) + .italic() + } else { + HStack { + Text("15m") + .font(.caption) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .leading) + Spacer() + Text("24h") + .font(.caption) + .foregroundColor(.secondary) + .frame(width: 80, alignment: .trailing) + } + } + } + .padding(.horizontal) + } + + // Indefinitely Toggle + HStack { + Toggle("Enable indefinitely", isOn: $enableIndefinitely) + .disabled(preset.duration != 0) // Disable for overrides with default durations + + Spacer() + } + .padding(.horizontal) + } + + // Action Buttons + VStack(spacing: 12) { + Button(action: { + let duration: TimeInterval? = enableIndefinitely ? nil : (durationHours * 3600) + onActivate(duration) + }) { + Text("Activate Override") + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .cornerRadius(10) + } + + Button(action: onCancel) { + Text("Cancel") + .font(.headline) + .foregroundColor(.secondary) + .frame(maxWidth: .infinity) + .padding() + .background(Color.gray.opacity(0.2)) + .cornerRadius(10) + } + } + .padding(.horizontal) + .padding(.bottom) + } + .navigationBarTitle("Activate Override", displayMode: .inline) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Cancel") { + onCancel() + } + } + } + } + } + + // Helper function to format duration in hours and minutes + private func formatDuration(_ hours: Double) -> String { + let totalMinutes = Int(hours * 60) + let hours = totalMinutes / 60 + let minutes = totalMinutes % 60 + + if hours > 0 && minutes > 0 { + return "\(hours)h \(minutes)m" + } else if hours > 0 { + return "\(hours)h" + } else { + return "\(minutes)m" + } + } +} + class OverridePresetsViewModel: ObservableObject { @Published var overridePresets: [OverridePreset] = [] @Published var isLoading = false @@ -199,9 +374,9 @@ class OverridePresetsViewModel: ObservableObject { @Published var alertType: AlertType? = nil @Published var statusMessage: String? = nil @Published var selectedPreset: OverridePreset? = nil + @Published var showOverrideModal = false enum AlertType { - case confirmActivation case confirmCancellation case statusSuccess case statusFailure @@ -228,13 +403,13 @@ class OverridePresetsViewModel: ObservableObject { } } - func activateOverride(preset: OverridePreset) async { + func activateOverride(preset: OverridePreset, duration: TimeInterval?) async { await MainActor.run { isActivating = true } do { - try await sendOverrideNotification(preset: preset) + try await sendOverrideNotification(preset: preset, duration: duration) await MainActor.run { self.isActivating = false self.statusMessage = "\(preset.name) override activated successfully." @@ -298,11 +473,11 @@ class OverridePresetsViewModel: ObservableObject { } } - private func sendOverrideNotification(preset: OverridePreset) async throws { + private func sendOverrideNotification(preset: OverridePreset, duration: TimeInterval?) async throws { let apnsService = LoopAPNSService() try await apnsService.sendOverrideNotification( presetName: preset.name, - duration: preset.duration + duration: duration ) } From d4023fdd54d6283e9ad7fa9d84373ee4ebea3767 Mon Sep 17 00:00:00 2001 From: codebymini Date: Sat, 30 Aug 2025 09:03:59 +0200 Subject: [PATCH 2/4] Add decimal to override targets for Loop if user setting is mmol --- LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index de919629d..2e9d2b8ce 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -157,7 +157,7 @@ struct OverridePresetRow: View { HStack(spacing: 8) { if let targetRange = preset.targetRange { - Text("Target: \(Int(targetRange.lowerBound))-\(Int(targetRange.upperBound))") + Text("Target: \(Localizer.formatLocalDouble(targetRange.lowerBound))-\(Localizer.formatLocalDouble(targetRange.upperBound))") .font(.caption) .foregroundColor(.secondary) } @@ -233,7 +233,7 @@ struct OverrideActivationModal: View { .multilineTextAlignment(.center) if let targetRange = preset.targetRange { - Text("Target: \(Int(targetRange.lowerBound))-\(Int(targetRange.upperBound))") + Text("Target: \(Localizer.formatLocalDouble(targetRange.lowerBound))-\(Localizer.formatLocalDouble(targetRange.upperBound))") .font(.subheadline) .foregroundColor(.secondary) } From a0f833f787f7bdffbe45ce8bdfbc17ebad1e557c Mon Sep 17 00:00:00 2001 From: codebymini Date: Sat, 30 Aug 2025 09:04:31 +0200 Subject: [PATCH 3/4] Change text for set durations and update ui --- LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index 2e9d2b8ce..8d28a66a1 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -255,7 +255,7 @@ struct OverrideActivationModal: View { HStack { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.orange) - Text("Overrides with default durations can't be set to indefinite") + Text("Overrides with a defined duration in Loop cannot be changed") .font(.caption) .foregroundColor(.orange) } @@ -268,6 +268,7 @@ struct OverrideActivationModal: View { HStack { Text("Duration") .font(.headline) + .foregroundColor(preset.duration != 0 ? .secondary : .primary) Spacer() Text(formatDuration(durationHours)) .font(.headline) @@ -304,6 +305,7 @@ struct OverrideActivationModal: View { HStack { Toggle("Enable indefinitely", isOn: $enableIndefinitely) .disabled(preset.duration != 0) // Disable for overrides with default durations + .foregroundColor(preset.duration != 0 ? .secondary : .primary) Spacer() } @@ -388,7 +390,7 @@ class OverridePresetsViewModel: ObservableObject { } do { - let presets = try await fetchOverridePresetsFromNightscout() + let presets = try await fetchOverridePresetsFromStorage() await MainActor.run { self.overridePresets = presets self.isLoading = false @@ -449,8 +451,7 @@ class OverridePresetsViewModel: ObservableObject { } } - private func fetchOverridePresetsFromNightscout() async throws -> [OverridePreset] { - // Use ProfileManager's already loaded overrides instead of fetching from Nightscout + private func fetchOverridePresetsFromStorage() async throws -> [OverridePreset] { let loopOverrides = ProfileManager.shared.loopOverrides return loopOverrides.map { override in From f068dc09c7feb7fd0b4671218ee967f28b5bfeba Mon Sep 17 00:00:00 2001 From: codebymini Date: Fri, 5 Sep 2025 20:24:25 +0200 Subject: [PATCH 4/4] Change UI for Loop overrides duration --- .../Remote/LoopAPNS/OverridePresetsView.swift | 89 +++++++++---------- 1 file changed, 40 insertions(+), 49 deletions(-) diff --git a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift index 8d28a66a1..7516c9f61 100644 --- a/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift +++ b/LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift @@ -208,10 +208,10 @@ struct OverrideActivationModal: View { // Initialize state based on preset duration if preset.duration == 0 { - // Indefinite override + // Indefinite override - allow user to choose _enableIndefinitely = State(initialValue: true) } else { - // Override with default duration + // Override with predefined duration - use preset duration _enableIndefinitely = State(initialValue: false) _durationHours = State(initialValue: preset.duration / 3600) } @@ -243,48 +243,35 @@ struct OverrideActivationModal: View { .font(.subheadline) .foregroundColor(.secondary) } - } - .padding(.top) - - Spacer() - // Duration Settings (moved to bottom for easier access) - VStack(spacing: 16) { - // Warning for overrides with default durations + // Only show duration for overrides with predefined duration if preset.duration != 0 { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.orange) - Text("Overrides with a defined duration in Loop cannot be changed") - .font(.caption) - .foregroundColor(.orange) - } - .padding(.horizontal) + Text("Duration: \(preset.durationDescription)") + .font(.subheadline) + .foregroundColor(.secondary) } + } + .padding(.top) - // Duration Input (shown when not indefinite or when override has default duration) - if !enableIndefinitely || preset.duration != 0 { - VStack(spacing: 8) { - HStack { - Text("Duration") - .font(.headline) - .foregroundColor(preset.duration != 0 ? .secondary : .primary) - Spacer() - Text(formatDuration(durationHours)) - .font(.headline) - .foregroundColor(preset.duration != 0 ? .secondary : .blue) - } + Spacer() - Slider(value: $durationHours, in: 0.25 ... max(24.0, preset.duration / 3600), step: 0.25) - .accentColor(.blue) - .disabled(preset.duration != 0) // Disable slider for overrides with default durations + // Duration Settings (only show for overrides without predefined duration) + if preset.duration == 0 { + VStack(spacing: 16) { + // Duration Input (only show when not indefinite) + if !enableIndefinitely { + VStack(spacing: 8) { + HStack { + Text("Duration") + .font(.headline) + Spacer() + Text(formatDuration(durationHours)) + .font(.headline) + .foregroundColor(.blue) + } - if preset.duration != 0 { - Text("Using preset's default duration") - .font(.caption) - .foregroundColor(.secondary) - .italic() - } else { + Slider(value: $durationHours, in: 0.25 ... 24.0, step: 0.25) + .accentColor(.blue) HStack { Text("15m") .font(.caption) @@ -297,25 +284,29 @@ struct OverrideActivationModal: View { .frame(width: 80, alignment: .trailing) } } + .padding(.horizontal) } - .padding(.horizontal) - } - - // Indefinitely Toggle - HStack { - Toggle("Enable indefinitely", isOn: $enableIndefinitely) - .disabled(preset.duration != 0) // Disable for overrides with default durations - .foregroundColor(preset.duration != 0 ? .secondary : .primary) - Spacer() + // Indefinitely Toggle + HStack { + Toggle("Enable indefinitely", isOn: $enableIndefinitely) + Spacer() + } + .padding(.horizontal) } - .padding(.horizontal) } // Action Buttons VStack(spacing: 12) { Button(action: { - let duration: TimeInterval? = enableIndefinitely ? nil : (durationHours * 3600) + let duration: TimeInterval? + if preset.duration == 0 { + // For indefinite overrides, use user selection + duration = enableIndefinitely ? nil : (durationHours * 3600) + } else { + // For overrides with predefined duration, use preset duration + duration = preset.duration + } onActivate(duration) }) { Text("Activate Override")