Skip to content

Commit eebeb66

Browse files
committed
Merge PR loopandlearn#457: Add modal for selecting duration of overrides for Loop
2 parents 69fe651 + f068dc0 commit eebeb66

File tree

1 file changed

+191
-24
lines changed

1 file changed

+191
-24
lines changed

LoopFollow/Remote/LoopAPNS/OverridePresetsView.swift

Lines changed: 191 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ struct OverridePresetsView: View {
6767
isActivating: viewModel.isActivating && viewModel.selectedPreset?.name == preset.name,
6868
onActivate: {
6969
viewModel.selectedPreset = preset
70-
viewModel.alertType = .confirmActivation
71-
viewModel.showAlert = true
70+
viewModel.showOverrideModal = true
7271
}
7372
)
7473
}
@@ -87,21 +86,24 @@ struct OverridePresetsView: View {
8786
await viewModel.loadOverridePresets()
8887
}
8988
}
90-
.alert(isPresented: $viewModel.showAlert) {
91-
switch viewModel.alertType {
92-
case .confirmActivation:
93-
return Alert(
94-
title: Text("Activate Override"),
95-
message: Text("Do you want to activate the override '\(viewModel.selectedPreset?.name ?? "")'?"),
96-
primaryButton: .default(Text("Confirm"), action: {
97-
if let preset = viewModel.selectedPreset {
98-
Task {
99-
await viewModel.activateOverride(preset: preset)
100-
}
89+
.sheet(isPresented: $viewModel.showOverrideModal) {
90+
if let preset = viewModel.selectedPreset {
91+
OverrideActivationModal(
92+
preset: preset,
93+
onActivate: { duration in
94+
viewModel.showOverrideModal = false
95+
Task {
96+
await viewModel.activateOverride(preset: preset, duration: duration)
10197
}
102-
}),
103-
secondaryButton: .cancel()
98+
},
99+
onCancel: {
100+
viewModel.showOverrideModal = false
101+
}
104102
)
103+
}
104+
}
105+
.alert(isPresented: $viewModel.showAlert) {
106+
switch viewModel.alertType {
105107
case .confirmCancellation:
106108
return Alert(
107109
title: Text("Cancel Override"),
@@ -155,7 +157,7 @@ struct OverridePresetRow: View {
155157

156158
HStack(spacing: 8) {
157159
if let targetRange = preset.targetRange {
158-
Text("Target: \(Int(targetRange.lowerBound))-\(Int(targetRange.upperBound))")
160+
Text("Target: \(Localizer.formatLocalDouble(targetRange.lowerBound))-\(Localizer.formatLocalDouble(targetRange.upperBound))")
159161
.font(.caption)
160162
.foregroundColor(.secondary)
161163
}
@@ -191,6 +193,172 @@ struct OverridePresetRow: View {
191193
}
192194
}
193195

196+
struct OverrideActivationModal: View {
197+
let preset: OverridePreset
198+
let onActivate: (TimeInterval?) -> Void
199+
let onCancel: () -> Void
200+
201+
@State private var enableIndefinitely: Bool
202+
@State private var durationHours: Double = 1.0
203+
204+
init(preset: OverridePreset, onActivate: @escaping (TimeInterval?) -> Void, onCancel: @escaping () -> Void) {
205+
self.preset = preset
206+
self.onActivate = onActivate
207+
self.onCancel = onCancel
208+
209+
// Initialize state based on preset duration
210+
if preset.duration == 0 {
211+
// Indefinite override - allow user to choose
212+
_enableIndefinitely = State(initialValue: true)
213+
} else {
214+
// Override with predefined duration - use preset duration
215+
_enableIndefinitely = State(initialValue: false)
216+
_durationHours = State(initialValue: preset.duration / 3600)
217+
}
218+
}
219+
220+
var body: some View {
221+
NavigationView {
222+
VStack(spacing: 20) {
223+
// Preset Info
224+
VStack(spacing: 12) {
225+
if let symbol = preset.symbol {
226+
Text(symbol)
227+
.font(.largeTitle)
228+
}
229+
230+
Text(preset.name)
231+
.font(.title2)
232+
.fontWeight(.semibold)
233+
.multilineTextAlignment(.center)
234+
235+
if let targetRange = preset.targetRange {
236+
Text("Target: \(Localizer.formatLocalDouble(targetRange.lowerBound))-\(Localizer.formatLocalDouble(targetRange.upperBound))")
237+
.font(.subheadline)
238+
.foregroundColor(.secondary)
239+
}
240+
241+
if let insulinNeedsScaleFactor = preset.insulinNeedsScaleFactor {
242+
Text("Insulin: \(Int(insulinNeedsScaleFactor * 100))%")
243+
.font(.subheadline)
244+
.foregroundColor(.secondary)
245+
}
246+
247+
// Only show duration for overrides with predefined duration
248+
if preset.duration != 0 {
249+
Text("Duration: \(preset.durationDescription)")
250+
.font(.subheadline)
251+
.foregroundColor(.secondary)
252+
}
253+
}
254+
.padding(.top)
255+
256+
Spacer()
257+
258+
// Duration Settings (only show for overrides without predefined duration)
259+
if preset.duration == 0 {
260+
VStack(spacing: 16) {
261+
// Duration Input (only show when not indefinite)
262+
if !enableIndefinitely {
263+
VStack(spacing: 8) {
264+
HStack {
265+
Text("Duration")
266+
.font(.headline)
267+
Spacer()
268+
Text(formatDuration(durationHours))
269+
.font(.headline)
270+
.foregroundColor(.blue)
271+
}
272+
273+
Slider(value: $durationHours, in: 0.25 ... 24.0, step: 0.25)
274+
.accentColor(.blue)
275+
HStack {
276+
Text("15m")
277+
.font(.caption)
278+
.foregroundColor(.secondary)
279+
.frame(width: 80, alignment: .leading)
280+
Spacer()
281+
Text("24h")
282+
.font(.caption)
283+
.foregroundColor(.secondary)
284+
.frame(width: 80, alignment: .trailing)
285+
}
286+
}
287+
.padding(.horizontal)
288+
}
289+
290+
// Indefinitely Toggle
291+
HStack {
292+
Toggle("Enable indefinitely", isOn: $enableIndefinitely)
293+
Spacer()
294+
}
295+
.padding(.horizontal)
296+
}
297+
}
298+
299+
// Action Buttons
300+
VStack(spacing: 12) {
301+
Button(action: {
302+
let duration: TimeInterval?
303+
if preset.duration == 0 {
304+
// For indefinite overrides, use user selection
305+
duration = enableIndefinitely ? nil : (durationHours * 3600)
306+
} else {
307+
// For overrides with predefined duration, use preset duration
308+
duration = preset.duration
309+
}
310+
onActivate(duration)
311+
}) {
312+
Text("Activate Override")
313+
.font(.headline)
314+
.foregroundColor(.white)
315+
.frame(maxWidth: .infinity)
316+
.padding()
317+
.background(Color.blue)
318+
.cornerRadius(10)
319+
}
320+
321+
Button(action: onCancel) {
322+
Text("Cancel")
323+
.font(.headline)
324+
.foregroundColor(.secondary)
325+
.frame(maxWidth: .infinity)
326+
.padding()
327+
.background(Color.gray.opacity(0.2))
328+
.cornerRadius(10)
329+
}
330+
}
331+
.padding(.horizontal)
332+
.padding(.bottom)
333+
}
334+
.navigationBarTitle("Activate Override", displayMode: .inline)
335+
.navigationBarTitleDisplayMode(.inline)
336+
.toolbar {
337+
ToolbarItem(placement: .navigationBarTrailing) {
338+
Button("Cancel") {
339+
onCancel()
340+
}
341+
}
342+
}
343+
}
344+
}
345+
346+
// Helper function to format duration in hours and minutes
347+
private func formatDuration(_ hours: Double) -> String {
348+
let totalMinutes = Int(hours * 60)
349+
let hours = totalMinutes / 60
350+
let minutes = totalMinutes % 60
351+
352+
if hours > 0 && minutes > 0 {
353+
return "\(hours)h \(minutes)m"
354+
} else if hours > 0 {
355+
return "\(hours)h"
356+
} else {
357+
return "\(minutes)m"
358+
}
359+
}
360+
}
361+
194362
class OverridePresetsViewModel: ObservableObject {
195363
@Published var overridePresets: [OverridePreset] = []
196364
@Published var isLoading = false
@@ -199,9 +367,9 @@ class OverridePresetsViewModel: ObservableObject {
199367
@Published var alertType: AlertType? = nil
200368
@Published var statusMessage: String? = nil
201369
@Published var selectedPreset: OverridePreset? = nil
370+
@Published var showOverrideModal = false
202371

203372
enum AlertType {
204-
case confirmActivation
205373
case confirmCancellation
206374
case statusSuccess
207375
case statusFailure
@@ -213,7 +381,7 @@ class OverridePresetsViewModel: ObservableObject {
213381
}
214382

215383
do {
216-
let presets = try await fetchOverridePresetsFromNightscout()
384+
let presets = try await fetchOverridePresetsFromStorage()
217385
await MainActor.run {
218386
self.overridePresets = presets
219387
self.isLoading = false
@@ -228,13 +396,13 @@ class OverridePresetsViewModel: ObservableObject {
228396
}
229397
}
230398

231-
func activateOverride(preset: OverridePreset) async {
399+
func activateOverride(preset: OverridePreset, duration: TimeInterval?) async {
232400
await MainActor.run {
233401
isActivating = true
234402
}
235403

236404
do {
237-
try await sendOverrideNotification(preset: preset)
405+
try await sendOverrideNotification(preset: preset, duration: duration)
238406
await MainActor.run {
239407
self.isActivating = false
240408
self.statusMessage = "\(preset.name) override activated successfully."
@@ -274,8 +442,7 @@ class OverridePresetsViewModel: ObservableObject {
274442
}
275443
}
276444

277-
private func fetchOverridePresetsFromNightscout() async throws -> [OverridePreset] {
278-
// Use ProfileManager's already loaded overrides instead of fetching from Nightscout
445+
private func fetchOverridePresetsFromStorage() async throws -> [OverridePreset] {
279446
let loopOverrides = ProfileManager.shared.loopOverrides
280447

281448
return loopOverrides.map { override in
@@ -298,11 +465,11 @@ class OverridePresetsViewModel: ObservableObject {
298465
}
299466
}
300467

301-
private func sendOverrideNotification(preset: OverridePreset) async throws {
468+
private func sendOverrideNotification(preset: OverridePreset, duration: TimeInterval?) async throws {
302469
let apnsService = LoopAPNSService()
303470
try await apnsService.sendOverrideNotification(
304471
presetName: preset.name,
305-
duration: preset.duration
472+
duration: duration
306473
)
307474
}
308475

0 commit comments

Comments
 (0)