polscm32 hai 1 ano
pai
achega
8d3104413b

+ 57 - 0
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
+                }
+            }
         }
     }
 

+ 46 - 40
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)
 
@@ -292,16 +301,13 @@ extension Bolus {
                                 Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
                             }
                         }.listRowBackground(Color.chart)
-
-                        Section {
-                            ForeCastChart(state: state, units: $state.units)
-                                .padding(.vertical)
-                        }.listRowBackground(Color.chart)
                     }
                 }
-                .safeAreaInset(edge: .bottom, spacing: 0) {
+                .safeAreaInset(edge: .bottom, content: {
                     stickyButton
-                }.blur(radius: state.waitForSuggestion ? 5 : 0)
+                })
+                .ignoresSafeArea(.keyboard, edges: .bottom)
+                .blur(radius: state.waitForSuggestion ? 5 : 0)
 
                 if state.waitForSuggestion {
                     CustomProgressView(text: progressText.rawValue)
@@ -396,15 +402,15 @@ extension Bolus {
 
         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

+ 0 - 41
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 {

+ 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."
 

+ 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
+        }
+    }
 }