Sfoglia il codice sorgente

Add back and fix trend arrow for LA lockscreen widget; cleanup

Deniz Cengiz 1 anno fa
parent
commit
20c91cae4a

+ 0 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -497,7 +497,6 @@
 		F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */; };
 		F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D5274B9A450037068D /* HealthKitStateModel.swift */; };
 		FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */; };
-		FE41E4D429463C660047FD55 /* NightscoutStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */; };
 		FE41E4D629463EE20047FD55 /* NightscoutPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */; };
 		FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */; };
 		FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */; };
@@ -1135,7 +1134,6 @@
 		F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleHealthKitRootView.swift; sourceTree = "<group>"; };
 		F90692D5274B9A450037068D /* HealthKitStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitStateModel.swift; sourceTree = "<group>"; };
 		FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorRootView.swift; sourceTree = "<group>"; };
-		FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutStatistics.swift; sourceTree = "<group>"; };
 		FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutPreferences.swift; sourceTree = "<group>"; };
 		FE66D16A291F74F8005D6F77 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
 		FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
@@ -1877,7 +1875,6 @@
 				CE82E02628E869DF00473A9C /* AlertEntry.swift */,
 				19B0EF2028F6D66200069496 /* Statistics.swift */,
 				19012CDB291D2CB900FB8210 /* LoopStats.swift */,
-				FE41E4D329463C660047FD55 /* NightscoutStatistics.swift */,
 				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
 				191F62672AD6B05A004D7911 /* NightscoutSettings.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
@@ -3479,7 +3476,6 @@
 				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,
 				BD1661312B82ADAB00256551 /* CustomProgressView.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
-				FE41E4D429463C660047FD55 /* NightscoutStatistics.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,

+ 1 - 1
FreeAPS/Sources/APS/APSManager.swift

@@ -1018,7 +1018,7 @@ final class BaseAPSManager: APSManager, Injectable {
             }
 
             // Insulin placeholder
-            var insulin = Ins(
+            let insulin = Ins(
                 TDD: 0,
                 bolus: 0,
                 temp_basal: 0,

+ 3 - 4
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -249,12 +249,12 @@ final class OpenAPS {
             if let bolusDTO = event.toBolusDTOEnum() {
                 eventDTOs.append(bolusDTO)
             }
-            if let tempBasalDTO = event.toTempBasalDTOEnum() {
-                eventDTOs.append(tempBasalDTO)
-            }
             if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
                 eventDTOs.append(tempBasalDurationDTO)
             }
+            if let tempBasalDTO = event.toTempBasalDTOEnum() {
+                eventDTOs.append(tempBasalDTO)
+            }
             return eventDTOs
         }
         return dtos
@@ -466,7 +466,6 @@ final class OpenAPS {
             let weighted_average = weight * average2hours + (1 - weight) * average14
 
             var duration: Decimal = 0
-            var newDuration: Decimal = 0
             var overrideTarget: Decimal = 0
 
             if useOverride {

+ 2 - 2
FreeAPS/Sources/APS/PluginManager.swift

@@ -26,10 +26,10 @@ class BasePluginManager: Injectable, PluginManager {
                 {
                     if let bundle = Bundle(url: pluginURL) {
                         if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
-                            debug(.deviceManager, "bundle name2:\(bname)")
+                            debug(.deviceManager, "bundle name: \(bname)")
                         }
                         if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
-                            debug(.deviceManager, "bundle is CGM")
+                            debug(.deviceManager, "bundle is CGM: \(bcgm)")
                         }
 
                         if bundle.isLoopPlugin {

+ 0 - 2
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -126,8 +126,6 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 
                 // Check if the fetched object is of the expected type
                 if let orefDetermination = orefDetermination {
-                    let forecastSet = orefDetermination.forecasts
-
                     result = Determination(
                         id: orefDetermination.id ?? UUID(),
                         reason: orefDetermination.reason ?? "",

+ 0 - 10
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -20,43 +20,33 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
         init?(from string: String) {
             switch string {
             case "\u{2191}\u{2191}\u{2191}",
-                 "↑↑↑",
                  "TripleUp":
                 self = .tripleUp
             case "\u{2191}\u{2191}",
-                 "↑↑",
                  "DoubleUp":
                 self = .doubleUp
             case "\u{2191}",
-                 "↑",
                  "SingleUp":
                 self = .singleUp
             case "\u{2197}",
-                 "↗︎",
                  "FortyFiveUp":
                 self = .fortyFiveUp
             case "\u{2192}",
-                 "→",
                  "Flat":
                 self = .flat
             case "\u{2198}",
-                 "↘︎",
                  "FortyFiveDown":
                 self = .fortyFiveDown
             case "\u{2193}",
-                 "↓",
                  "SingleDown":
                 self = .singleDown
             case "\u{2193}\u{2193}",
-                 "↓↓",
                  "DoubleDown":
                 self = .doubleDown
             case "\u{2193}\u{2193}\u{2193}",
-                 "↓↓↓",
                  "TripleDown":
                 self = .tripleDown
             case "\u{2194}",
-                 "↔︎",
                  "NONE":
                 self = .none
             case "NOT COMPUTABLE":

+ 0 - 6
FreeAPS/Sources/Models/NightscoutStatistics.swift

@@ -1,6 +0,0 @@
-import Foundation
-
-struct NightscoutStatistics: JSON {
-    let report = "statistics"
-    let dailystats: Statistics?
-}

+ 64 - 7
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -110,6 +110,7 @@ extension Bolus {
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var forecastDisplayType: ForecastDisplayType = .cone
         @Published var smooth: Bool = false
+        @Published var stops: [Gradient.Stop] = []
 
         let now = Date.now
 
@@ -438,6 +439,54 @@ extension Bolus {
             }
         }
 
+        private func calculateGradientStops() async -> [Gradient.Stop] {
+            let low = Double(lowGlucose)
+            let high = Double(highGlucose)
+
+            let glucoseValues = glucoseFromPersistence
+                .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
+
+            let minimum = glucoseValues.min() ?? 0.0
+            let maximum = glucoseValues.max() ?? 0.0
+
+            // Handle edge case where minimum and maximum are equal
+            guard minimum != maximum else {
+                return [
+                    Gradient.Stop(color: .green, location: 0.0),
+                    Gradient.Stop(color: .green, location: 1.0)
+                ]
+            }
+
+            // Calculate positions for gradient
+            let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
+                (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+            let highPosition = (high - Double(truncating: minimum as NSNumber)) /
+                (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
+
+            // Ensure positions are in bounds [0, 1]
+            let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
+            let clampedHighPosition = max(0.0, min(highPosition, 1.0))
+
+            // Ensure lowPosition is less than highPosition
+            let epsilon: CGFloat = 0.0001
+            let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
+            var adjustedHighPosition = sortedPositions[1]
+
+            if adjustedHighPosition - sortedPositions[0] < epsilon {
+                adjustedHighPosition = min(1.0, sortedPositions[0] + epsilon)
+            }
+
+            return [
+                Gradient.Stop(color: .red, location: 0.0),
+                Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
+                Gradient.Stop(color: .green, location: sortedPositions[0] + epsilon),
+                // draw green above lowGlucose till highGlucose
+                Gradient.Stop(color: .green, location: adjustedHighPosition),
+                Gradient.Stop(color: .orange, location: adjustedHighPosition + epsilon), // draw orange above highGlucose
+                Gradient.Stop(color: .orange, location: 1.0)
+            ]
+        }
+
         // MARK: - Carbs
 
         func saveMeal() async {
@@ -575,6 +624,14 @@ extension Bolus.StateModel {
             let ids = await self.fetchGlucose()
             let glucoseObjects: [GlucoseStored] = await CoreDataStack.shared.getNSManagedObject(with: ids, context: viewContext)
             await updateGlucoseArray(with: glucoseObjects)
+
+            if smooth {
+                let newStops = await self.calculateGradientStops()
+
+                await MainActor.run {
+                    self.stops = newStops
+                }
+            }
         }
     }
 
@@ -724,20 +781,20 @@ extension Bolus.StateModel {
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         guard minCount > 0 else { return }
 
-        let (minResult, maxResult) = await Task.detached {
-            let minForecast = (0 ..< self.minCount).map { index in
+        async let minForecastResult = Task.detached {
+            (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
             }
+        }.value
 
-            let maxForecast = (0 ..< self.minCount).map { index in
+        async let maxForecastResult = Task.detached {
+            (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
             }
-
-            return (minForecast, maxForecast)
         }.value
 
-        minForecast = minResult
-        maxForecast = maxResult
+        minForecast = await minForecastResult
+        maxForecast = await maxForecastResult
     }
 }
 

+ 57 - 66
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -171,39 +171,48 @@ extension Bolus {
                 VStack {
                     Form {
                         Section {
+                            ForeCastChart(state: state, units: $state.units, stops: state.stops)
+                                .padding(.vertical)
+                        }.listRowBackground(Color.chart)
+
+                        Section {
                             carbsTextField()
 
-                            if state.useFPUconversion {
-                                proteinAndFat()
-                            }
+                            DisclosureGroup("Extras") {
+                                if state.useFPUconversion {
+                                    proteinAndFat()
+                                }
 
-                            // Time
-                            HStack {
-                                Text("Time").foregroundStyle(Color.secondary)
-                                Spacer()
-                                if !pushed {
-                                    Button {
-                                        pushed = true
-                                    } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
-                                        .padding(.trailing, 5)
-                                } else {
-                                    Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
-                                    label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
-                                    DatePicker(
-                                        "Time",
-                                        selection: $state.date,
-                                        displayedComponents: [.hourAndMinute]
-                                    ).controlSize(.mini)
-                                        .labelsHidden()
-                                    Button {
-                                        state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                // Time
+                                HStack {
+                                    Text("Time").foregroundStyle(Color.secondary)
+                                    Spacer()
+                                    if !pushed {
+                                        Button {
+                                            pushed = true
+                                        } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
+                                            .padding(.trailing, 5)
+                                    } else {
+                                        Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
+                                        label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+                                        DatePicker(
+                                            "Time",
+                                            selection: $state.date,
+                                            displayedComponents: [.hourAndMinute]
+                                        ).controlSize(.mini)
+                                            .labelsHidden()
+                                        Button {
+                                            state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                        }
+                                        label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                     }
-                                    label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                 }
-                            }
-                            HStack {
-                                Image(systemName: "square.and.pencil").foregroundColor(.secondary)
-                                TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+
+                                // Notes
+                                HStack {
+                                    Image(systemName: "square.and.pencil").foregroundColor(.secondary)
+                                    TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+                                }
                             }
                         }.listRowBackground(Color.chart)
 
@@ -293,15 +302,10 @@ extension Bolus {
                             }
                         }.listRowBackground(Color.chart)
 
-                        Section {
-                            ForeCastChart(state: state, units: $state.units)
-                                .padding(.vertical)
-                        }.listRowBackground(Color.chart)
+                        treatmentButton
                     }
                 }
-                .safeAreaInset(edge: .bottom, spacing: 0) {
-                    stickyButton
-                }.blur(radius: state.waitForSuggestion ? 5 : 0)
+                .blur(radius: state.waitForSuggestion ? 5 : 0)
 
                 if state.waitForSuggestion {
                     CustomProgressView(text: progressText.rawValue)
@@ -365,46 +369,33 @@ extension Bolus {
             }
         }
 
-        var stickyButton: some View {
-            ZStack {
-                Rectangle()
-                    .frame(width: UIScreen.main.bounds.width, height: 120).offset(y: 40)
-                    .shadow(
-                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
-                            Color.black.opacity(0.33),
-                        radius: 3
-                    )
-                    .foregroundStyle(Color.chart)
-
-                Button {
-                    state.invokeTreatmentsTask()
-                } label: {
-                    taskButtonLabel
-                        .font(.headline)
-                        .foregroundStyle(Color.white)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .frame(minHeight: 50)
-                }
-                .disabled(disableTaskButton)
-                .background(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .padding()
-                .offset(y: 20)
+        var treatmentButton: some View {
+            Button {
+                state.invokeTreatmentsTask()
+            } label: {
+                taskButtonLabel
+                    .font(.headline)
+                    .foregroundStyle(Color.white)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .frame(height: 35)
             }
+            .disabled(disableTaskButton)
+            .listRowBackground(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
+            .shadow(radius: 3)
+            .clipShape(RoundedRectangle(cornerRadius: 8))
         }
 
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
-                return Text("Max Bolus of \(state.maxBolus) U Exceeded")
+                return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
             } else if externalBolusLimitExceeded {
-                return Text("Max External Bolus of \(state.maxExternal) U Exceeded")
+                return Text("Max External Bolus of \(state.maxExternal.description) U Exceeded")
             } else if carbLimitExceeded {
-                return Text("Max Carbs of \(state.maxCarbs) g Exceeded")
+                return Text("Max Carbs of \(state.maxCarbs.description) g Exceeded")
             } else if fatLimitExceeded {
-                return Text("Max Fat of \(state.maxFat) g Exceeded")
+                return Text("Max Fat of \(state.maxFat.description) g Exceeded")
             } else if proteinLimitExceeded {
-                return Text("Max Protein of \(state.maxProtein) g Exceeded")
+                return Text("Max Protein of \(state.maxProtein.description) g Exceeded")
             }
 
             let hasInsulin = state.amount > 0

+ 37 - 33
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -7,6 +7,7 @@ struct ForeCastChart: View {
     @StateObject var state: Bolus.StateModel
     @Environment(\.colorScheme) var colorScheme
     @Binding var units: GlucoseUnits
+    var stops: [Gradient.Stop]
 
     @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
 
@@ -35,6 +36,42 @@ struct ForeCastChart: View {
 
     var body: some View {
         VStack {
+            HStack {
+                HStack {
+                    Text("Added carbs: ")
+                        .font(.footnote)
+                        .fontWeight(.bold)
+                        .foregroundStyle(.orange)
+
+                    Text("\(state.carbs.description) g")
+                        .font(.footnote)
+                        .foregroundStyle(.orange)
+                }
+                .padding(8)
+                .background {
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(Color.orange.opacity(0.2))
+                }
+
+                Spacer()
+
+                HStack {
+                    Text("Added insulin: ")
+                        .font(.footnote)
+                        .fontWeight(.bold)
+                        .foregroundStyle(.blue)
+
+                    Text("\(state.amount.description) U")
+                        .font(.footnote)
+                        .foregroundStyle(.blue)
+                }
+                .padding(8)
+                .background {
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(Color.blue.opacity(0.2))
+                }
+            }
+
             forecastChart
                 .padding(.vertical, 3)
             HStack {
@@ -83,39 +120,6 @@ struct ForeCastChart: View {
         .backport.chartForegroundStyleScale(state: state)
     }
 
-    private var stops: [Gradient.Stop] {
-        let low = Double(state.lowGlucose)
-        let high = Double(state.highGlucose)
-
-        let glucoseValues = state.glucoseFromPersistence
-            .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
-
-        let minimum = glucoseValues.min() ?? 0.0
-        let maximum = glucoseValues.max() ?? 0.0
-
-        // Calculate positions for gradient
-        let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
-            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
-        let highPosition = (high - Double(truncating: minimum as NSNumber)) /
-            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
-
-        // Ensure positions are in bounds [0, 1]
-        let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
-        let clampedHighPosition = max(0.0, min(highPosition, 1.0))
-
-        // Ensure lowPosition is less than highPosition
-        let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
-
-        return [
-            Gradient.Stop(color: .red, location: 0.0),
-            Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
-            Gradient.Stop(color: .green, location: sortedPositions[0] + 0.0001), // draw green above lowGlucose till highGlucose
-            Gradient.Stop(color: .green, location: sortedPositions[1]),
-            Gradient.Stop(color: .orange, location: sortedPositions[1] + 0.0001), // draw orange above highGlucose
-            Gradient.Stop(color: .orange, location: 1.0)
-        ]
-    }
-
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL

+ 2 - 43
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -200,47 +200,6 @@ struct MainChartView: View {
 
 // MARK: - Components
 
-struct Backport<Content: View> {
-    let content: Content
-}
-
-extension View {
-    var backport: Backport<Self> { Backport(content: self) }
-}
-
-extension Backport {
-    @ViewBuilder func chartXSelection(value: Binding<Date?>) -> some View {
-        if #available(iOS 17, *) {
-            content.chartXSelection(value: value)
-        } else {
-            content
-        }
-    }
-
-    @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
-        if (state as? Bolus.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
-            (state as? Home.StateModel)?.forecastDisplayType == ForecastDisplayType.lines
-        {
-            let modifiedContent = content
-                .chartForegroundStyleScale([
-                    "iob": .blue,
-                    "uam": Color.uam,
-                    "zt": Color.zt,
-                    "cob": .orange
-                ])
-
-            if state is Home.StateModel {
-                modifiedContent
-                    .chartLegend(.hidden)
-            } else {
-                modifiedContent
-            }
-        } else {
-            content
-        }
-    }
-}
-
 extension MainChartView {
     /// empty chart that just shows the Y axis and Y grid lines. Created separately from `mainChart` to allow main chart to scroll horizontally while having a fixed Y axis
     private var staticYAxisChart: some View {
@@ -1216,8 +1175,8 @@ extension MainChartView {
         // Ensure maxForecast is not more than 100 over maxGlucose
         let adjustedMaxForecast = min(maxForecast, maxGlucose + 100)
 
-        var minOverall = min(minGlucose, minForecast)
-        var maxOverall = max(maxGlucose, adjustedMaxForecast)
+        let minOverall = min(minGlucose, minForecast)
+        let maxOverall = max(maxGlucose, adjustedMaxForecast)
 
         minValue = minOverall - 50
         maxValue = maxOverall + 80

+ 31 - 39
FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivityBottomRowConfiguration.swift

@@ -13,6 +13,7 @@ struct LiveActivityBottomRowConfiguration: BaseView {
     @State private var showAddItemDialog: Bool = false
     @State private var isEditMode: Bool = false
     @State private var draggingItem: LiveActivityItem?
+    @State private var showDeleteAlert: Bool = false
 
     @Environment(\.colorScheme) var colorScheme
 
@@ -130,11 +131,16 @@ struct LiveActivityBottomRowConfiguration: BaseView {
                                             )
                                         )
                                         .disabled(!isEditMode)
-                                    // TODO: fix the jiggle modifier to make use of animation
-//                                        .jiggle(amount: 2, isEnabled: showItemDeleteButtons)
+                                        .rotationEffect(.degrees(isEditMode ? 2.5 : 0))
+                                        .rotation3DEffect(.degrees(isEditMode ? 2.5 : 0), axis: (x: 0, y: -5, z: 0))
+                                        .animation(
+                                            isEditMode ? Animation.easeInOut(duration: 0.15)
+                                                .repeatForever(autoreverses: true) : .default,
+                                            value: isEditMode
+                                        )
                                     if isEditMode {
                                         Button(action: {
-                                            removeItem(item)
+                                            showDeleteAlert = true
                                         }) {
                                             Image(systemName: "minus.circle.fill")
                                                 .foregroundColor(Color(UIColor.systemGray2)) // Opaque foreground color
@@ -143,6 +149,16 @@ struct LiveActivityBottomRowConfiguration: BaseView {
                                                 .font(.system(size: 20))
                                         }
                                         .offset(x: -45, y: -10)
+                                        .alert(isPresented: $showDeleteAlert) {
+                                            Alert(
+                                                title: Text("Delete Widget"),
+                                                message: Text("Are you sure you want to delete this widget?"),
+                                                primaryButton: .destructive(Text("Delete")) {
+                                                    removeItem(item)
+                                                },
+                                                secondaryButton: .cancel()
+                                            )
+                                        }
                                     }
                                 }
                                 .animation(.easeInOut, value: draggingItem)
@@ -304,10 +320,12 @@ struct LiveActivityBottomRowConfiguration: BaseView {
             selectedItems = LiveActivityItem.defaultItems
             saveOrder()
         }
+        print("Loaded order: \(selectedItems.map(\.rawValue))")
         updateVisibilityForSelectedItems()
     }
 
     private func saveOrder() {
+        print("Saving order: \(selectedItems.map(\.rawValue))")
         UserDefaults.standard.saveLiveActivityOrder(selectedItems)
     }
 
@@ -346,14 +364,6 @@ struct LiveActivityBottomRowConfiguration: BaseView {
             setItemVisibility(item: item, isVisible: false)
         }
     }
-
-    @ViewBuilder func jiggle(amount: Double = 2, isEnabled: Bool = true) -> some View {
-        if isEnabled {
-            modifier(JiggleViewModifier(amount: amount))
-        } else {
-            self
-        }
-    }
 }
 
 struct DropViewDelegate: DropDelegate {
@@ -371,6 +381,12 @@ struct DropViewDelegate: DropDelegate {
             withAnimation {
                 items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex > fromIndex ? toIndex + 1 : toIndex)
             }
+
+            // Save to User Defaults
+            saveOrder()
+
+            // Trigger Live Activity Update
+            Foundation.NotificationCenter.default.post(name: .liveActivityOrderDidChange, object: nil)
         }
     }
 
@@ -378,6 +394,10 @@ struct DropViewDelegate: DropDelegate {
         draggingItem = nil
         return true
     }
+
+    private func saveOrder() {
+        UserDefaults.standard.saveLiveActivityOrder(items)
+    }
 }
 
 // Extension for UserDefaults to save and load the order
@@ -447,31 +467,3 @@ struct DummyChartGroupBoxStyle: GroupBoxStyle {
 extension GroupBoxStyle where Self == DummyChartGroupBoxStyle {
     static var dummyChart: DummyChartGroupBoxStyle { .init() }
 }
-
-struct JiggleViewModifier: ViewModifier {
-    let amount: Double
-
-    @State private var isJiggling = false
-
-    func body(content: Content) -> some View {
-        content
-            .rotationEffect(.degrees(isJiggling ? amount : 0))
-            .animation(
-                .easeInOut(duration: randomize(interval: 0.14, withVariance: 0.025))
-                    .repeatForever(autoreverses: true),
-                value: isJiggling
-            )
-            .animation(
-                .easeInOut(duration: randomize(interval: 0.18, withVariance: 0.025))
-                    .repeatForever(autoreverses: true),
-                value: isJiggling
-            )
-            .onAppear {
-                isJiggling.toggle()
-            }
-    }
-
-    private func randomize(interval: TimeInterval, withVariance variance: Double) -> TimeInterval {
-        interval + variance * (Double.random(in: 500 ... 1000) / 500)
-    }
-}

+ 2 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -15,6 +15,8 @@ struct LiveActivityAttributes: ActivityAttributes {
         let showCurrentGlucose: Bool
         let showUpdatedLabel: Bool
 
+        let itemOrder: [String]
+
         /// true for the first state that is set on the activity
         let isInitialState: Bool
     }

+ 14 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -1,5 +1,15 @@
 import Foundation
 
+extension UserDefaults {
+    private enum Keys {
+        static let liveActivityOrder = "liveActivityOrder"
+    }
+
+    func loadLiveActivityOrderFromUserDefaults() -> [String]? {
+        array(forKey: Keys.liveActivityOrder) as? [String]
+    }
+}
+
 extension LiveActivityAttributes.ContentState {
     static func formatGlucose(_ value: Int, units: GlucoseUnits, forceSign: Bool) -> String {
         let formatter = NumberFormatter()
@@ -100,6 +110,9 @@ extension LiveActivityAttributes.ContentState {
             detailedState = nil
         }
 
+        let itemOrder = UserDefaults.standard
+            .loadLiveActivityOrderFromUserDefaults() ?? ["currentGlucose", "iob", "cob", "updatedLabel"]
+
         self.init(
             bg: formattedBG,
             direction: trendString,
@@ -110,6 +123,7 @@ extension LiveActivityAttributes.ContentState {
             showIOB: settings.showIOB,
             showCurrentGlucose: settings.showCurrentGlucose,
             showUpdatedLabel: settings.showUpdatedLabel,
+            itemOrder: itemOrder,
             isInitialState: false
         )
     }

+ 31 - 0
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -70,6 +70,12 @@ import UIKit
             .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
                 self?.forceActivityUpdate()
             }
+        notificationCenter.addObserver(
+            self,
+            selector: #selector(handleLiveActivityOrderChange),
+            name: .liveActivityOrderDidChange,
+            object: nil
+        )
     }
 
     // TODO: - use a delegate or a custom notification here instead
@@ -124,6 +130,30 @@ import UIKit
         }
     }
 
+    @objc private func handleLiveActivityOrderChange() {
+        Task {
+            await self.updateLiveActivityOrder()
+        }
+    }
+
+    @MainActor private func updateLiveActivityOrder() async {
+        guard let latestGlucose = latestGlucose else { return }
+
+        let content = LiveActivityAttributes.ContentState(
+            new: latestGlucose,
+            prev: latestGlucose,
+            units: settings.units,
+            chart: glucoseFromPersistence ?? [],
+            settings: settings,
+            determination: determination,
+            override: isOverridesActive
+        )
+
+        if let content = content {
+            await pushUpdate(content)
+        }
+    }
+
     private func setupGlucoseArray() {
         Task {
             // Fetch and map glucose to GlucoseData struct
@@ -206,6 +236,7 @@ import UIKit
                         showIOB: true,
                         showCurrentGlucose: true,
                         showUpdatedLabel: true,
+                        itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
                         isInitialState: true
                     ),
                     staleDate: Date.now.addingTimeInterval(60)

+ 1 - 1
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -496,7 +496,7 @@ extension NightscoutAPI {
         }
         request.httpMethod = "POST"
 
-        let (data, response) = try await URLSession.shared.data(for: request)
+        let (_, response) = try await URLSession.shared.data(for: request)
 
         // Check the response status code
         guard let httpResponse = response as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {

+ 1 - 1
FreeAPS/Sources/Services/UnlockManager/UnlockManager.swift

@@ -10,7 +10,7 @@ struct UnlockError: Error {
 }
 
 final class BaseUnlockManager: UnlockManager {
-    func unlock() async throws -> Bool {
+    @MainActor func unlock() async throws -> Bool {
         let context = LAContext()
         let reason = "We need to make sure you are the owner of the device."
 

+ 0 - 2
FreeAPS/Sources/Shortcuts/State/ListStateIntent.swift

@@ -33,8 +33,6 @@ import Foundation
             cob: iob_cob.cob,
             unit: stateIntent.settingsManager.settings.units
         )
-        let iob_text = String(format: "%.2f", iob_cob.iob)
-        let cob_text = String(format: "%.2f", iob_cob.cob)
         return .result(
             value: BG,
             view: ListStateView(state: BG)

+ 39 - 0
FreeAPS/Sources/Views/ViewModifiers.swift

@@ -162,4 +162,43 @@ extension View {
     }
 
     func asAny() -> AnyView { .init(self) }
+
+    var backport: Backport<Self> { Backport(content: self) }
+}
+
+struct Backport<Content: View> {
+    let content: Content
+}
+
+extension Backport {
+    @ViewBuilder func chartXSelection(value: Binding<Date?>) -> some View {
+        if #available(iOS 17, *) {
+            content.chartXSelection(value: value)
+        } else {
+            content
+        }
+    }
+
+    @ViewBuilder func chartForegroundStyleScale(state: any StateModel) -> some View {
+        if (state as? Bolus.StateModel)?.forecastDisplayType == ForecastDisplayType.lines ||
+            (state as? Home.StateModel)?.forecastDisplayType == ForecastDisplayType.lines
+        {
+            let modifiedContent = content
+                .chartForegroundStyleScale([
+                    "iob": .blue,
+                    "uam": Color.uam,
+                    "zt": Color.zt,
+                    "cob": .orange
+                ])
+
+            if state is Home.StateModel {
+                modifiedContent
+                    .chartLegend(.hidden)
+            } else {
+                modifiedContent
+            }
+        } else {
+            content
+        }
+    }
 }

+ 36 - 50
LiveActivity/LiveActivity.swift

@@ -98,10 +98,6 @@ struct LiveActivity: Widget {
         additionalState: LiveActivityAttributes.ContentAdditionalState
     ) -> some View {
         VStack(spacing: 2) {
-//            Image(systemName: "fork.knife")
-//                .font(.title3)
-//                .foregroundColor(.yellow)
-
             HStack {
                 Text(
                     carbsFormatter.string(from: additionalState.cob as NSNumber) ?? "--"
@@ -119,10 +115,6 @@ struct LiveActivity: Widget {
         additionalState: LiveActivityAttributes.ContentAdditionalState
     ) -> some View {
         VStack(spacing: 2) {
-//            Image(systemName: "syringe.fill")
-//                .font(.title3)
-//                .foregroundColor(.blue)
-
             HStack {
                 Text(
                     bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
@@ -222,7 +214,6 @@ struct LiveActivity: Widget {
                 .fontWeight(.bold)
                 .font(.title3)
                 .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-//            Text(additionalState.unit).foregroundStyle(.primary).font(.footnote)
         }
     }
 
@@ -285,25 +276,6 @@ struct LiveActivity: Widget {
         return (stack, characters)
     }
 
-    @ViewBuilder func trendArrow(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        let gradient = LinearGradient(colors: [
-            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
-            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
-            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
-            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
-        ], startPoint: .leading, endPoint: .trailing)
-
-        if !context.isStale {
-            Image(systemName: "arrowshape.right.circle")
-                .font(.headline)
-                .rotationEffect(.degrees(additionalState.rotationDegrees))
-//                .foregroundStyle(gradient)
-        }
-    }
-
     @ViewBuilder func chart(
         context: ActivityViewContext<LiveActivityAttributes>,
         additionalState: LiveActivityAttributes.ContentAdditionalState
@@ -312,8 +284,8 @@ struct LiveActivity: Widget {
             Text("No data available")
         } else {
             // Determine scale
-            let min = min(additionalState.chart.min() ?? 45, 40) - 20
-            let max = max(additionalState.chart.max() ?? 270, 300) + 50
+            let minValue = min(additionalState.chart.min() ?? 45, 40) - 20
+            let maxValue = max(additionalState.chart.max() ?? 270, 300) + 50
 
             let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? additionalState.lowGlucose : additionalState.lowGlucose
                 .asMmolL
@@ -352,7 +324,7 @@ struct LiveActivity: Widget {
                     AxisValueLabel().foregroundStyle(.primary).font(.footnote)
                 }
             }
-//            .chartYScale(domain: additionalState.unit == "mg/dL" ? min ... max : min.asMmolL ... max.asMmolL)
+            .chartYScale(domain: additionalState.unit == "mg/dL" ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
             .chartYAxis(.hidden)
             .chartPlotStyle { plotContent in
                 plotContent
@@ -378,29 +350,37 @@ struct LiveActivity: Widget {
                     .frame(height: 80)
 
                 HStack {
-                    if context.state.showCurrentGlucose {
-                        VStack {
-                            bgLabel(context: context, additionalState: detailedViewState)
-                            HStack {
-                                changeLabel(context: context)
+                    ForEach(context.state.itemOrder, id: \.self) { item in
+                        switch item {
+                        case "currentGlucose":
+                            if context.state.showCurrentGlucose {
+                                VStack {
+                                    bgLabel(context: context, additionalState: detailedViewState)
+                                    HStack {
+                                        changeLabel(context: context)
+                                        if !context.isStale, let direction = context.state.direction {
+                                            Text(direction).font(.headline)
+                                        }
+                                    }
+                                }
+                            }
+                        case "iob":
+                            if context.state.showIOB {
+                                iobLabel(context: context, additionalState: detailedViewState)
                             }
+                        case "cob":
+                            if context.state.showCOB {
+                                cobLabel(context: context, additionalState: detailedViewState)
+                            }
+                        case "updatedLabel":
+                            if context.state.showUpdatedLabel {
+                                updatedLabel(context: context)
+                            }
+                        default:
+                            EmptyView()
                         }
                         Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
                     }
-
-                    if context.state.showIOB {
-                        iobLabel(context: context, additionalState: detailedViewState)
-                        Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
-                    }
-
-                    if context.state.showCOB {
-                        cobLabel(context: context, additionalState: detailedViewState)
-                        Divider().foregroundStyle(.primary).fontWeight(.bold).frame(width: 10)
-                    }
-
-                    if context.state.showUpdatedLabel {
-                        updatedLabel(context: context)
-                    }
                 }
             })
                 .privacySensitive()
@@ -520,6 +500,7 @@ private extension LiveActivityAttributes.ContentState {
             showIOB: true,
             showCurrentGlucose: true,
             showUpdatedLabel: true,
+            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
             isInitialState: false
         )
     }
@@ -535,6 +516,7 @@ private extension LiveActivityAttributes.ContentState {
             showIOB: true,
             showCurrentGlucose: true,
             showUpdatedLabel: true,
+            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
             isInitialState: false
         )
     }
@@ -550,6 +532,7 @@ private extension LiveActivityAttributes.ContentState {
             showIOB: true,
             showCurrentGlucose: true,
             showUpdatedLabel: true,
+            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
             isInitialState: false
         )
     }
@@ -566,6 +549,7 @@ private extension LiveActivityAttributes.ContentState {
             showIOB: true,
             showCurrentGlucose: true,
             showUpdatedLabel: true,
+            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
             isInitialState: false
         )
     }
@@ -581,6 +565,7 @@ private extension LiveActivityAttributes.ContentState {
             showIOB: true,
             showCurrentGlucose: true,
             showUpdatedLabel: true,
+            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
             isInitialState: false
         )
     }
@@ -596,6 +581,7 @@ private extension LiveActivityAttributes.ContentState {
             showIOB: true,
             showCurrentGlucose: true,
             showUpdatedLabel: true,
+            itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
             isInitialState: true
         )
     }

+ 2 - 2
Model/CoreDataStack.swift

@@ -409,7 +409,7 @@ extension CoreDataStack {
         with ids: [NSManagedObjectID],
         context: NSManagedObjectContext
     ) async -> [T] {
-        await Task { () -> [T] in
+        await context.perform {
             var objects = [T]()
             do {
                 for id in ids {
@@ -421,7 +421,7 @@ extension CoreDataStack {
                 debugPrint("Failed to fetch objects: \(error.localizedDescription)")
             }
             return objects
-        }.value
+        }
     }
 }
 

+ 1 - 0
Model/Helper/CustomNotification.swift

@@ -7,4 +7,5 @@ extension Notification.Name {
     static let didUpdateDetermination = Notification.Name("didUpdateDetermination")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
     static let didUpdateCobIob = Notification.Name("didUpdateCobIob")
+    static let liveActivityOrderDidChange = Notification.Name("liveActivityOrderDidChange")
 }

+ 1 - 55
Trio.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -1,5 +1,5 @@
 {
-  "originHash" : "f5c836c216c4ca7d356e3777e58d6d4f9502b03f3974891349eb775f4c4cf750",
+  "originHash" : "cef813f4bbb01679d4ac9bf4a9f82c1a0a61e44dc839643e81aa92e4d00642bc",
   "pins" : [
     {
       "identity" : "cryptoswift",
@@ -11,15 +11,6 @@
       }
     },
     {
-      "identity" : "mkringprogressview",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/maxkonovalov/MKRingProgressView.git",
-      "state" : {
-        "branch" : "master",
-        "revision" : "660888aab1d2ab0ed7eb9eb53caec12af4955fa7"
-      }
-    },
-    {
       "identity" : "slidebutton",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/no-comment/SlideButton",
@@ -29,24 +20,6 @@
       }
     },
     {
-      "identity" : "swift-algorithms",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/apple/swift-algorithms",
-      "state" : {
-        "revision" : "2327673b0e9c7e90e6b1826376526ec3627210e4",
-        "version" : "0.2.1"
-      }
-    },
-    {
-      "identity" : "swift-numerics",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/apple/swift-numerics",
-      "state" : {
-        "revision" : "6583ac70c326c3ee080c1d42d9ca3361dca816cd",
-        "version" : "0.1.0"
-      }
-    },
-    {
       "identity" : "swiftcharts",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/ivanschuetz/SwiftCharts",
@@ -56,33 +29,6 @@
       }
     },
     {
-      "identity" : "swiftdate",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/malcommac/SwiftDate",
-      "state" : {
-        "revision" : "6190d0cefff3013e77ed567e6b074f324e5c5bf5",
-        "version" : "6.3.1"
-      }
-    },
-    {
-      "identity" : "swiftmessages",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/SwiftKickMobile/SwiftMessages",
-      "state" : {
-        "revision" : "62e12e138fc3eedf88c7553dd5d98712aa119f40",
-        "version" : "9.0.9"
-      }
-    },
-    {
-      "identity" : "swinject",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/Swinject/Swinject",
-      "state" : {
-        "revision" : "be9dbcc7b86811bc131539a20c6f9c2d3e56919f",
-        "version" : "2.9.1"
-      }
-    },
-    {
       "identity" : "tidepoolkit",
       "kind" : "remoteSourceControl",
       "location" : "https://github.com/tidepool-org/TidepoolKit",