Forráskód Böngészése

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into watch

Deniz Cengiz 1 éve
szülő
commit
338827330b

+ 2 - 2
Trio Watch App Extension/Views/GlucoseChartView.swift

@@ -56,7 +56,7 @@ struct GlucoseChartView: View {
                     }
                 }
                 .chartXAxis(.hidden)
-                .chartYAxisLabel("\(timeWindow.rawValue)h", alignment: .topLeading)
+                .chartYAxisLabel("\(timeWindow.rawValue) h", alignment: .topLeading)
                 .chartYAxis {
                     AxisMarks(position: .trailing) { value in
                         AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
@@ -64,7 +64,7 @@ struct GlucoseChartView: View {
 
                         AxisValueLabel {
                             if let glucose = value.as(Double.self) {
-                                Text("\(Int(glucose))").font(.system(size: 8))
+                                Text("\(Int(glucose))")
                             }
                         }
                     }

+ 8 - 0
Trio.xcodeproj/project.pbxproj

@@ -490,6 +490,8 @@
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */; };
 		DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */; };
+		DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */; };
+		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -1212,6 +1214,8 @@
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageHelpView.swift; sourceTree = "<group>"; };
+		DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+Overrides.swift"; sourceTree = "<group>"; };
+		DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsRootView+TempTargets.swift"; sourceTree = "<group>"; };
 		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
@@ -2461,6 +2465,7 @@
 		BD793CAD2CE7660C00D669AC /* Overrides */ = {
 			isa = PBXGroup;
 			children = (
+				DDAA29822D2D1D7B006546A1 /* AdjustmentsRootView+Overrides.swift */,
 				DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */,
 				DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */,
 				DDD163192C4C695E00CD525A /* EditOverrideForm.swift */,
@@ -2474,6 +2479,7 @@
 				DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */,
 				58A3D5392C96D4DE003F90FC /* AddTempTargetForm.swift */,
 				5825A1BD2C97335C0046467E /* EditTempTargetForm.swift */,
+				DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */,
 			);
 			path = TempTargets;
 			sourceTree = "<group>";
@@ -3636,6 +3642,7 @@
 				3811DEE825CA063400A708ED /* Injected.swift in Sources */,
 				DD1745152C54388A00211FAC /* TherapySettingsView.swift in Sources */,
 				585E2CAE2BE7BF46006ECF1A /* PumpEvent+helper.swift in Sources */,
+				DDAA29832D2D1D93006546A1 /* AdjustmentsRootView+Overrides.swift in Sources */,
 				DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */,
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
@@ -3887,6 +3894,7 @@
 				CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */,
 				E592A3772CEEC038009A472C /* ContactImageStateModel.swift in Sources */,
 				E592A3782CEEC038009A472C /* ContactImageDataFlow.swift in Sources */,
+				DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */,
 				E592A3792CEEC038009A472C /* ContactImageRootView.swift in Sources */,
 				BDC531182D1062F200088832 /* ContactImageState.swift in Sources */,
 				E592A37A2CEEC038009A472C /* ContactImageProvider.swift in Sources */,

+ 3 - 3
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -12,7 +12,7 @@ extension Adjustments.StateModel {
             overrideToEnact?.enabled = true
             overrideToEnact?.date = Date()
             overrideToEnact?.isUploadedToNS = false
-            isEnabled = true
+            isOverrideEnabled = true
 
             await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
             await resetStateVariables()
@@ -201,8 +201,8 @@ extension Adjustments.StateModel {
             let result = try IDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? OverrideStored
             }
-            isEnabled = result.first?.enabled ?? false
-            if !isEnabled {
+            isOverrideEnabled = result.first?.enabled ?? false
+            if !isOverrideEnabled {
                 await resetStateVariables()
             }
         } catch {

+ 1 - 1
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -25,7 +25,7 @@ extension Adjustments.StateModel {
                 try viewContext.existingObject(with: id) as? TempTargetStored
             }
             isTempTargetEnabled = result.first?.enabled ?? false
-            if !isEnabled {
+            if !isOverrideEnabled {
                 await resetTempTargetState()
             }
         } catch {

+ 1 - 1
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -16,7 +16,7 @@ extension Adjustments {
         // MARK: - Override and Temp Target Properties
 
         var overridePercentage: Double = 100
-        var isEnabled = false
+        var isOverrideEnabled = false
         var indefinite = true
         var overrideDuration: Decimal = 0
         var target: Decimal = 0

+ 42 - 477
Trio/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -6,25 +6,34 @@ extension Adjustments {
     struct RootView: BaseView {
         let resolver: Resolver
         @State var state = StateModel()
-        @State private var isEditing = false
-        @State private var showOverrideCreationSheet = false
-        @State private var showTempTargetCreationSheet = false
-        @State private var showingDetail = false
-        @State private var showCheckmark: Bool = false
-        @State private var selectedPresetID: String?
-        @State private var selectedTempTargetPresetID: String?
-        @State private var selectedOverride: OverrideStored?
-        @State private var selectedTempTarget: TempTargetStored?
-        @State private var isConfirmDeletePresented = false
-        @State private var isPromptPresented = false
-        @State private var isRemoveAlertPresented = false
-        @State private var removeAlert: Alert?
-        @State private var isEditingTT = false
+        @State var isEditing = false
+        @State var showOverrideCreationSheet = false
+        @State var showTempTargetCreationSheet = false
+        @State var showingDetail = false
+        @State var showOverrideCheckmark: Bool = false
+        @State var showTempTargetCheckmark: Bool = false
+        @State var selectedOverridePresetID: String?
+        @State var selectedTempTargetPresetID: String?
+        @State var selectedOverride: OverrideStored?
+        @State var selectedTempTarget: TempTargetStored?
+        @State var isConfirmDeletePresented = false
+        @State var isPromptPresented = false
+        @State var isRemoveAlertPresented = false
+        @State var removeAlert: Alert?
+        @State var isEditingTT = false
+
+        private var shouldDisplayStickyOverrideStopButton: Bool {
+            state.isOverrideEnabled && state.activeOverrideName.isNotEmpty
+        }
+
+        private var shouldDisplayStickyTempTargetStopButton: Bool {
+            state.isTempTargetEnabled && state.activeTempTargetName.isNotEmpty
+        }
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(AppState.self) var appState
 
-        private func formattedGlucose(glucose: Decimal) -> String {
+        func formattedGlucose(glucose: Decimal) -> String {
             let formattedValue: String
             if state.units == .mgdL {
                 formattedValue = Formatter.glucoseFormatter(for: state.units)
@@ -55,7 +64,18 @@ extension Adjustments {
                     .background(appState.trioBackgroundColor(for: colorScheme))
                 }
                 .listSectionSpacing(10)
-                .safeAreaInset(edge: .bottom, spacing: 30) { stickyStopButton }
+                .safeAreaInset(
+                    edge: .bottom,
+                    spacing: shouldDisplayStickyOverrideStopButton || shouldDisplayStickyTempTargetStopButton ? 30 : 0
+                ) {
+                    if shouldDisplayStickyOverrideStopButton, state.selectedTab == .overrides {
+                        stickyStopOverrideButton
+                    } else if shouldDisplayStickyTempTargetStopButton, state.selectedTab == .tempTargets {
+                        stickyStopTempTargetButton
+                    } else {
+                        EmptyView()
+                    }
+                }
                 .scrollContentBackground(.hidden)
                 .background(appState.trioBackgroundColor(for: colorScheme))
                 .onAppear(perform: configureView)
@@ -126,32 +146,7 @@ extension Adjustments {
             }).background(appState.trioBackgroundColor(for: colorScheme))
         }
 
-        @ViewBuilder func overrides() -> some View {
-            if state.isEnabled, state.activeOverrideName.isNotEmpty {
-                currentActiveAdjustment
-            }
-            if state.overridePresets.isNotEmpty {
-                overridePresets
-            } else {
-                defaultText
-            }
-        }
-
-        @ViewBuilder func tempTargets() -> some View {
-            if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
-                currentActiveAdjustment
-            }
-            if state.scheduledTempTargets.isNotEmpty {
-                scheduledTempTargets
-            }
-            if state.tempTargetPresets.isNotEmpty {
-                tempTargetPresets
-            } else {
-                defaultText
-            }
-        }
-
-        private var defaultText: some View {
+        var defaultText: some View {
             switch state.selectedTab {
             case .overrides:
                 Section {} header: {
@@ -170,193 +165,7 @@ extension Adjustments {
             }
         }
 
-        private var overridePresets: some View {
-            Section {
-                ForEach(state.overridePresets) { preset in
-                    overridesView(for: preset)
-                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                            Button(role: .none) {
-                                selectedOverride = preset
-                                isConfirmDeletePresented = true
-                            } label: {
-                                Label("Delete", systemImage: "trash")
-                                    .tint(.red)
-                            }
-                            Button(action: {
-                                // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
-                                selectedOverride = preset
-                                state.showOverrideEditSheet = true
-                            }, label: {
-                                Label("Edit", systemImage: "pencil")
-                                    .tint(.blue)
-                            })
-                        }
-                }
-                .onMove(perform: state.reorderOverride)
-                .confirmationDialog(
-                    "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
-                    isPresented: $isConfirmDeletePresented,
-                    titleVisibility: .visible
-                ) {
-                    if let itemToDelete = selectedOverride {
-                        Button(
-                            state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
-                            role: .destructive
-                        ) {
-                            if state.currentActiveOverride == selectedOverride {
-                                Task {
-                                    // Save cancelled Override in OverrideRunStored Entity
-                                    // Cancel ALL active Override
-                                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
-                                }
-                            }
-                            // Perform the delete action
-                            Task {
-                                await state.invokeOverridePresetDeletion(itemToDelete.objectID)
-                            }
-                            // Reset the selected item after deletion
-                            selectedOverride = nil
-                        }
-                    }
-                    Button("Cancel", role: .cancel) {
-                        // Dismiss the dialog without action
-                        selectedOverride = nil
-                    }
-                } message: {
-                    if state.currentActiveOverride == selectedOverride {
-                        Text(
-                            state
-                                .currentActiveOverride == selectedOverride ?
-                                "This override preset is currently running. Deleting will stop it." : ""
-                        )
-                    }
-                }
-                .listRowBackground(Color.chart)
-            } header: {
-                Text("Override Presets")
-            } footer: {
-                HStack {
-                    Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                    Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
-                }
-            }
-        }
-
-        private var scheduledTempTargets: some View {
-            Section {
-                ForEach(state.scheduledTempTargets) { tempTarget in
-                    tempTargetView(for: tempTarget)
-                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                            swipeActions(for: tempTarget)
-                        }
-                }
-                .listRowBackground(Color.chart)
-            } header: {
-                Text("Scheduled Temp Targets")
-            }
-        }
-
-        private var tempTargetPresets: some View {
-            Section {
-                ForEach(state.tempTargetPresets) { preset in
-                    tempTargetView(for: preset, showCheckmark: showCheckmark) {
-                        enactTempTargetPreset(preset)
-                    }
-                    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
-                        swipeActions(for: preset)
-                    }
-                }
-                .onMove(perform: state.reorderTempTargets)
-                .confirmationDialog(
-                    deleteConfirmationTitle,
-                    isPresented: $isConfirmDeletePresented,
-                    titleVisibility: .visible
-                ) {
-                    deleteConfirmationButtons()
-                } message: {
-                    deleteConfirmationMessage
-                }
-                .listRowBackground(Color.chart)
-            } header: {
-                Text("Temporary Target Presets")
-            } footer: {
-                HStack {
-                    Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
-                    Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
-                }
-            }
-        }
-
-        private func enactTempTargetPreset(_ preset: TempTargetStored) {
-            Task {
-                let objectID = preset.objectID
-                await state.enactTempTargetPreset(withID: objectID)
-                selectedTempTargetPresetID = preset.id?.uuidString
-                showCheckmark.toggle()
-
-                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                    showCheckmark = false
-                }
-            }
-        }
-
-        private func swipeActions(for tempTarget: TempTargetStored) -> some View {
-            Group {
-                Button {
-                    Task {
-                        selectedTempTarget = tempTarget
-                        isConfirmDeletePresented = true
-                    }
-                } label: {
-                    Label("Delete", systemImage: "trash")
-                        .tint(.red)
-                }
-                Button(action: {
-                    selectedTempTarget = tempTarget
-                    state.showTempTargetEditSheet = true
-                }, label: {
-                    Label("Edit", systemImage: "pencil")
-                        .tint(.blue)
-                })
-            }
-        }
-
-        private var deleteConfirmationTitle: String {
-            "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
-        }
-
-        private func deleteConfirmationButtons() -> some View {
-            Group {
-                if let itemToDelete = selectedTempTarget {
-                    Button(
-                        state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
-                        role: .destructive
-                    ) {
-                        if state.currentActiveTempTarget == selectedTempTarget {
-                            Task {
-                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-                            }
-                        }
-                        Task {
-                            await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
-                        }
-                        selectedTempTarget = nil
-                    }
-                }
-                Button("Cancel", role: .cancel) {
-                    selectedTempTarget = nil
-                }
-            }
-        }
-
-        private var deleteConfirmationMessage: Text? {
-            if state.currentActiveTempTarget == selectedTempTarget {
-                return Text("This Temp Target preset is currently running. Deleting will stop it.")
-            }
-            return nil
-        }
-
-        private var currentActiveAdjustment: some View {
+        var currentActiveAdjustment: some View {
             switch state.selectedTab {
             case .overrides:
                 Section {
@@ -411,60 +220,7 @@ extension Adjustments {
             }
         }
 
-        var stickyStopButton: some View {
-            ZStack {
-                Rectangle()
-                    .frame(width: UIScreen.main.bounds.width, height: 65)
-                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
-                    .background(.thinMaterial)
-                    .opacity(0.8)
-                    .clipShape(Rectangle())
-
-                Group {
-                    switch state.selectedTab {
-                    case .overrides:
-                        Button(action: {
-                            Task {
-                                // Save cancelled Override in OverrideRunStored Entity
-                                // Cancel ALL active Override
-                                await state.disableAllActiveOverrides(createOverrideRunEntry: true)
-                            }
-                        }, label: {
-                            Text("Stop Override")
-                                .frame(maxWidth: .infinity, maxHeight: .infinity)
-                                .padding(10)
-                        })
-                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
-                            .disabled(!state.isEnabled)
-                            .background(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
-                            .tint(.white)
-                            .clipShape(RoundedRectangle(cornerRadius: 8))
-                    case .tempTargets:
-                        Button(action: {
-                            Task {
-                                // Save cancelled Temp Targets in TempTargetRunStored Entity
-                                // Cancel ALL active Temp Targets
-                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
-                                // Update View
-                                state.updateLatestTempTargetConfiguration()
-                            }
-                        }, label: {
-                            Text("Stop Temp Target")
-                                .frame(maxWidth: .infinity, maxHeight: .infinity)
-                                .padding(10)
-                        })
-                            .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
-                            .disabled(!state.isTempTargetEnabled)
-                            .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
-                            .tint(.white)
-                            .clipShape(RoundedRectangle(cornerRadius: 8))
-                    }
-                }
-                .padding(5)
-            }
-        }
-
-        private var cancelAdjustmentButton: some View {
+        var cancelAdjustmentButton: some View {
             switch state.selectedTab {
             case .overrides:
                 Button(action: {
@@ -478,8 +234,8 @@ extension Adjustments {
 
                 })
                     .frame(maxWidth: .infinity, alignment: .center)
-                    .disabled(!state.isEnabled)
-                    .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
+                    .disabled(!state.isOverrideEnabled)
+                    .listRowBackground(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
                     .tint(.white)
             case .tempTargets:
                 Button(action: {
@@ -502,75 +258,7 @@ extension Adjustments {
             }
         }
 
-        private func tempTargetView(
-            for tempTarget: TempTargetStored,
-            showCheckmark: Bool = false,
-            onTap: (() -> Void)? = nil
-        ) -> some View {
-            let target = tempTarget.target ?? 100
-            let tempTargetValue = Decimal(target as! Double.RawValue)
-            let isSelected = tempTarget.id?.uuidString == selectedPresetID
-            let tempTargetHalfBasal = Decimal(
-                tempTarget.halfBasalTarget as? Double
-                    .RawValue ?? Double(state.settingHalfBasalTarget)
-            )
-            let percentage = Int(
-                state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
-            )
-            let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
-
-            return ZStack(alignment: .trailing) {
-                HStack {
-                    VStack(alignment: .leading) {
-                        HStack {
-                            Text(tempTarget.name ?? "")
-                            Spacer()
-                            if remainingTime > 0 {
-                                Text("Starts in \(formattedTimeRemaining(remainingTime))")
-                                    .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
-                            }
-                        }
-                        HStack(spacing: 2) {
-                            Text(formattedGlucose(glucose: target as Decimal))
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            Text("for")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            Text("min")
-                                .foregroundColor(.secondary)
-                                .font(.caption)
-                            if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
-                                Text(", \(percentage)%")
-                                    .foregroundColor(.secondary)
-                                    .font(.caption)
-                            }
-                            Spacer()
-                        }
-                        .padding(.top, 2)
-                    }
-                    .contentShape(Rectangle())
-                    .onTapGesture {
-                        onTap?()
-                    }
-                }
-                if showCheckmark && isSelected {
-                    Image(systemName: "checkmark.circle.fill")
-                        .imageScale(.large)
-                        .fontWeight(.bold)
-                        .foregroundStyle(Color.green)
-                } else if onTap != nil {
-                    Image(systemName: "line.3.horizontal")
-                        .imageScale(.medium)
-                        .foregroundStyle(.secondary)
-                }
-            }
-        }
-
-        private func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
+        func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
             let totalSeconds = Int(timeInterval)
             let hours = totalSeconds / 3600
             let minutes = (totalSeconds % 3600) / 60
@@ -584,128 +272,5 @@ extension Adjustments {
                 return "<1m"
             }
         }
-
-        private var overrideLabelDivider: some View {
-            Divider()
-                .frame(width: 1, height: 20)
-        }
-
-        @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
-            let isSelected = preset.id == selectedPresetID
-            let name = preset.name ?? ""
-            let indefinite = preset.indefinite
-            let duration = preset.duration?.decimalValue ?? Decimal(0)
-            let percentage = preset.percentage
-            let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
-            let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
-
-            let target: String = {
-                guard let targetValue = preset.target, targetValue != 0 else { return "" }
-                return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
-            }()
-
-            let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
-
-            let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
-
-            let scheduledSMBString: String = {
-                guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
-                return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
-            }()
-
-            let smbString: String = {
-                guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
-                return "SMBs Off\(scheduledSMBString)"
-            }()
-
-            let maxSmbMinsString: String = {
-                guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
-                      smbMinutes != state.defaultSmbMinutes else { return "" }
-                return "\(smbMinutes.formatted()) min SMB"
-            }()
-
-            let maxUamMinsString: String = {
-                guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
-                      uamMinutes != state.defaultUamMinutes else { return "" }
-                return "\(uamMinutes.formatted()) min UAM"
-            }()
-
-            let isfAndCrString: String = {
-                switch (preset.isfAndCr, preset.isf, preset.cr) {
-                case (_, true, true),
-                     (true, _, _):
-                    return " ISF/CR"
-                case (false, true, false):
-                    return " ISF"
-                case (false, false, true):
-                    return " CR"
-                default:
-                    return ""
-                }
-            }()
-
-            let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
-
-            // Combine all labels into a single array, filtering out empty strings
-            let labels: [String] = [
-                durationString,
-                percentageString,
-                targetString,
-                smbString,
-                maxSmbMinsString,
-                maxUamMinsString
-            ].filter { !$0.isEmpty }
-
-            if !name.isEmpty {
-                ZStack(alignment: .trailing) {
-                    HStack {
-                        VStack {
-                            HStack {
-                                Text(name)
-                                Spacer()
-                            }
-                            HStack(spacing: 5) {
-                                ForEach(labels, id: \.self) { label in
-                                    Text(label)
-                                    if label != labels.last { // Add divider between labels
-                                        overrideLabelDivider
-                                    }
-                                }
-                                Spacer()
-                            }
-                            .padding(.top, 2)
-                            .foregroundColor(.secondary)
-                            .font(.caption)
-                        }
-                        .contentShape(Rectangle())
-                        .onTapGesture {
-                            Task {
-                                let objectID = preset.objectID
-                                await state.enactOverridePreset(withID: objectID)
-                                state.hideModal()
-                                showCheckmark.toggle()
-                                selectedPresetID = preset.id
-
-                                // Deactivate checkmark after 3 seconds
-                                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                                    showCheckmark = false
-                                }
-                            }
-                        }
-                    }
-                    // show checkmark to indicate if the preset was actually pressed
-                    if showCheckmark && isSelected {
-                        Image(systemName: "checkmark.circle.fill")
-                            .imageScale(.large)
-                            .fontWeight(.bold)
-                            .foregroundStyle(Color.green)
-                    } else {
-                        Image(systemName: "line.3.horizontal")
-                            .imageScale(.medium)
-                            .foregroundStyle(.secondary)
-                    }
-                }
-            }
-        }
     }
 }

+ 1 - 1
Trio/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift

@@ -398,7 +398,7 @@ struct AddOverrideForm: View {
                     Button(action: {
                         Task {
                             if state.indefinite { state.overrideDuration = 0 }
-                            state.isEnabled.toggle()
+                            state.isOverrideEnabled.toggle()
                             await state.saveCustomOverride()
                             await state.resetStateVariables()
                             dismiss()

+ 256 - 0
Trio/Sources/Modules/Adjustments/View/Overrides/AdjustmentsRootView+Overrides.swift

@@ -0,0 +1,256 @@
+import CoreData
+import SwiftUI
+
+extension Adjustments.RootView {
+    @ViewBuilder func overrides() -> some View {
+        if state.isOverrideEnabled, state.activeOverrideName.isNotEmpty {
+            currentActiveAdjustment
+        }
+        if state.overridePresets.isNotEmpty {
+            overridePresets
+        } else {
+            defaultText
+        }
+    }
+
+    var overridePresets: some View {
+        Section {
+            ForEach(state.overridePresets) { preset in
+                overridesView(for: preset, showCheckMark: showOverrideCheckmark) {
+                    enactOverridePreset(preset)
+                }
+                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                    swipeActionsForOverrides(for: preset)
+                }
+            }
+            .onMove(perform: state.reorderOverride)
+            .confirmationDialog(
+                "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
+                isPresented: $isConfirmDeletePresented,
+                titleVisibility: .visible
+            ) {
+                if let itemToDelete = selectedOverride {
+                    Button(
+                        state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
+                        role: .destructive
+                    ) {
+                        if state.currentActiveOverride == selectedOverride {
+                            Task {
+                                // Save cancelled Override in OverrideRunStored Entity
+                                // Cancel ALL active Override
+                                await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                            }
+                        }
+                        // Perform the delete action
+                        Task {
+                            await state.invokeOverridePresetDeletion(itemToDelete.objectID)
+                        }
+                        // Reset the selected item after deletion
+                        selectedOverride = nil
+                    }
+                }
+                Button("Cancel", role: .cancel) {
+                    // Dismiss the dialog without action
+                    selectedOverride = nil
+                }
+            } message: {
+                if state.currentActiveOverride == selectedOverride {
+                    Text(
+                        state
+                            .currentActiveOverride == selectedOverride ?
+                            "This override preset is currently running. Deleting will stop it." : ""
+                    )
+                }
+            }
+            .listRowBackground(Color.chart)
+        } header: {
+            Text("Override Presets")
+        } footer: {
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
+                Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
+            }
+        }
+    }
+
+    func enactOverridePreset(_ preset: OverrideStored) {
+        Task {
+            let objectID = preset.objectID
+            await state.enactOverridePreset(withID: objectID)
+            state.hideModal()
+            selectedOverridePresetID = preset.id
+            showOverrideCheckmark = true
+
+            // Deactivate checkmark after 3 seconds
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                showOverrideCheckmark = false
+            }
+        }
+    }
+
+    func swipeActionsForOverrides(for preset: OverrideStored) -> some View {
+        Group {
+            Button(role: .none) {
+                selectedOverride = preset
+                isConfirmDeletePresented = true
+            } label: {
+                Label("Delete", systemImage: "trash")
+                    .tint(.red)
+            }
+            Button(action: {
+                // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
+                selectedOverride = preset
+                state.showOverrideEditSheet = true
+            }, label: {
+                Label("Edit", systemImage: "pencil")
+                    .tint(.blue)
+            })
+        }
+    }
+
+    var overrideLabelDivider: some View {
+        Divider()
+            .frame(width: 1, height: 20)
+    }
+
+    var stickyStopOverrideButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                Task {
+                    // Save cancelled Override in OverrideRunStored Entity
+                    // Cancel ALL active Override
+                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                }
+            }, label: {
+                Text("Stop Override")
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                .disabled(!state.isOverrideEnabled)
+                .background(!state.isOverrideEnabled ? Color(.systemGray4) : Color(.systemRed))
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+
+                .padding(5)
+        }
+    }
+
+    @ViewBuilder func overridesView(
+        for preset: OverrideStored,
+        showCheckMark _: Bool = false,
+        onTap: (() -> Void)? = nil
+    ) -> some View {
+        let isSelected = preset.id == selectedOverridePresetID
+        let name = preset.name ?? ""
+        let indefinite = preset.indefinite
+        let duration = preset.duration?.decimalValue ?? Decimal(0)
+        let percentage = preset.percentage
+        let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
+        let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
+
+        let target: String = {
+            guard let targetValue = preset.target, targetValue != 0 else { return "" }
+            return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
+        }()
+
+        let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
+
+        let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
+
+        let scheduledSMBString: String = {
+            guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
+            return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
+        }()
+
+        let smbString: String = {
+            guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
+            return "SMBs Off\(scheduledSMBString)"
+        }()
+
+        let maxSmbMinsString: String = {
+            guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
+                  smbMinutes != state.defaultSmbMinutes else { return "" }
+            return "\(smbMinutes.formatted()) min SMB"
+        }()
+
+        let maxUamMinsString: String = {
+            guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
+                  uamMinutes != state.defaultUamMinutes else { return "" }
+            return "\(uamMinutes.formatted()) min UAM"
+        }()
+
+        let isfAndCrString: String = {
+            switch (preset.isfAndCr, preset.isf, preset.cr) {
+            case (_, true, true),
+                 (true, _, _):
+                return " ISF/CR"
+            case (false, true, false):
+                return " ISF"
+            case (false, false, true):
+                return " CR"
+            default:
+                return ""
+            }
+        }()
+
+        let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
+
+        // Combine all labels into a single array, filtering out empty strings
+        let labels: [String] = [
+            durationString,
+            percentageString,
+            targetString,
+            smbString,
+            maxSmbMinsString,
+            maxUamMinsString
+        ].filter { !$0.isEmpty }
+
+        if !name.isEmpty {
+            ZStack(alignment: .trailing) {
+                HStack {
+                    VStack {
+                        HStack {
+                            Text(name)
+                            Spacer()
+                        }
+                        HStack(spacing: 5) {
+                            ForEach(labels, id: \.self) { label in
+                                Text(label)
+                                if label != labels.last { // Add divider between labels
+                                    overrideLabelDivider
+                                }
+                            }
+                            Spacer()
+                        }
+                        .padding(.top, 2)
+                        .foregroundColor(.secondary)
+                        .font(.caption)
+                    }
+                    .contentShape(Rectangle())
+                    .onTapGesture {
+                        onTap?()
+                    }
+                }
+                // show checkmark to indicate if the preset was actually pressed
+                if showOverrideCheckmark && isSelected {
+                    Image(systemName: "checkmark.circle.fill")
+                        .imageScale(.large)
+                        .fontWeight(.bold)
+                        .foregroundStyle(Color.green)
+                } else {
+                    Image(systemName: "line.3.horizontal")
+                        .imageScale(.medium)
+                        .foregroundStyle(.secondary)
+                }
+            }
+        }
+    }
+}

+ 231 - 0
Trio/Sources/Modules/Adjustments/View/TempTargets/AdjustmentsRootView+TempTargets.swift

@@ -0,0 +1,231 @@
+import CoreData
+import SwiftUI
+
+extension Adjustments.RootView {
+    @ViewBuilder func tempTargets() -> some View {
+        if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
+            currentActiveAdjustment
+        }
+        if state.scheduledTempTargets.isNotEmpty {
+            scheduledTempTargets
+        }
+        if state.tempTargetPresets.isNotEmpty {
+            tempTargetPresets
+        } else {
+            defaultText
+        }
+    }
+
+    private var scheduledTempTargets: some View {
+        Section {
+            ForEach(state.scheduledTempTargets) { tempTarget in
+                tempTargetView(for: tempTarget)
+                    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                        swipeActionsForTempTargets(for: tempTarget)
+                    }
+            }
+            .listRowBackground(Color.chart)
+        } header: {
+            Text("Scheduled Temp Targets")
+        }
+    }
+
+    private var tempTargetPresets: some View {
+        Section {
+            ForEach(state.tempTargetPresets) { preset in
+                tempTargetView(for: preset, showCheckmark: showTempTargetCheckmark) {
+                    enactTempTargetPreset(preset)
+                }
+                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                    swipeActionsForTempTargets(for: preset)
+                }
+            }
+            .onMove(perform: state.reorderTempTargets)
+            .confirmationDialog(
+                deleteConfirmationTitle,
+                isPresented: $isConfirmDeletePresented,
+                titleVisibility: .visible
+            ) {
+                deleteConfirmationButtons()
+            } message: {
+                deleteConfirmationMessage
+            }
+            .listRowBackground(Color.chart)
+        } header: {
+            Text("Temporary Target Presets")
+        } footer: {
+            HStack {
+                Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
+                Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
+            }
+        }
+    }
+
+    private func enactTempTargetPreset(_ preset: TempTargetStored) {
+        Task {
+            let objectID = preset.objectID
+            await state.enactTempTargetPreset(withID: objectID)
+            selectedTempTargetPresetID = preset.id?.uuidString
+            showTempTargetCheckmark = true
+
+            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                showTempTargetCheckmark = false
+            }
+        }
+    }
+
+    private func swipeActionsForTempTargets(for tempTarget: TempTargetStored) -> some View {
+        Group {
+            Button {
+                Task {
+                    selectedTempTarget = tempTarget
+                    isConfirmDeletePresented = true
+                }
+            } label: {
+                Label("Delete", systemImage: "trash")
+                    .tint(.red)
+            }
+            Button(action: {
+                selectedTempTarget = tempTarget
+                state.showTempTargetEditSheet = true
+            }, label: {
+                Label("Edit", systemImage: "pencil")
+                    .tint(.blue)
+            })
+        }
+    }
+
+    private var deleteConfirmationTitle: String {
+        "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
+    }
+
+    private func deleteConfirmationButtons() -> some View {
+        Group {
+            if let itemToDelete = selectedTempTarget {
+                Button(
+                    state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
+                    role: .destructive
+                ) {
+                    if state.currentActiveTempTarget == selectedTempTarget {
+                        Task {
+                            await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                        }
+                    }
+                    Task {
+                        await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
+                    }
+                    selectedTempTarget = nil
+                }
+            }
+            Button("Cancel", role: .cancel) {
+                selectedTempTarget = nil
+            }
+        }
+    }
+
+    private var deleteConfirmationMessage: Text? {
+        if state.currentActiveTempTarget == selectedTempTarget {
+            return Text("This Temp Target preset is currently running. Deleting will stop it.")
+        }
+        return nil
+    }
+
+    var stickyStopTempTargetButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                Task {
+                    // Save cancelled Temp Targets in TempTargetRunStored Entity
+                    // Cancel ALL active Temp Targets
+                    await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                    // Update View
+                    state.updateLatestTempTargetConfiguration()
+                }
+            }, label: {
+                Text("Stop Temp Target")
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, height: 40, alignment: .center)
+                .disabled(!state.isTempTargetEnabled)
+                .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding(5)
+        }
+    }
+
+    private func tempTargetView(
+        for tempTarget: TempTargetStored,
+        showCheckmark: Bool = false,
+        onTap: (() -> Void)? = nil
+    ) -> some View {
+        let target = tempTarget.target ?? 100
+        let tempTargetValue = Decimal(target as! Double.RawValue)
+        let isSelected = tempTarget.id?.uuidString == selectedTempTargetPresetID
+        let tempTargetHalfBasal = Decimal(
+            tempTarget.halfBasalTarget as? Double
+                .RawValue ?? Double(state.settingHalfBasalTarget)
+        )
+        let percentage = Int(
+            state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
+        )
+        let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
+
+        return ZStack(alignment: .trailing) {
+            HStack {
+                VStack(alignment: .leading) {
+                    HStack {
+                        Text(tempTarget.name ?? "")
+                        Spacer()
+                        if remainingTime > 0 {
+                            Text("Starts in \(formattedTimeRemaining(remainingTime))")
+                                .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                        }
+                    }
+                    HStack(spacing: 2) {
+                        Text(formattedGlucose(glucose: target as Decimal))
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("for")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        Text("min")
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
+                            Text(", \(percentage)%")
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                        }
+                        Spacer()
+                    }
+                    .padding(.top, 2)
+                }
+                .contentShape(Rectangle())
+                .onTapGesture {
+                    onTap?()
+                }
+            }
+            if showCheckmark && isSelected {
+                Image(systemName: "checkmark.circle.fill")
+                    .imageScale(.large)
+                    .fontWeight(.bold)
+                    .foregroundStyle(Color.green)
+            } else if onTap != nil {
+                Image(systemName: "line.3.horizontal")
+                    .imageScale(.medium)
+                    .foregroundStyle(.secondary)
+            }
+        }
+    }
+}