Преглед изворни кода

Merge pull request #172 from dnzxy/extend-chart-legend

Extend Chart Legend Sheet
polscm32 пре 1 година
родитељ
комит
fb85ffbda2

+ 5 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -486,6 +486,7 @@
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
+		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.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 */; };
@@ -1062,9 +1063,8 @@
 		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
 		BDFD16592AE40438007F0DDA /* TreatmentsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TreatmentsRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
-		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
-		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* TreatmentsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsProvider.swift; sourceTree = "<group>"; };
+		C2A0A42E2CE0312C003B98E8 /* ConstantValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantValues.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C8D1A7CA8C10C4403D4BBFA7 /* TreatmentsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TreatmentsDataFlow.swift; sourceTree = "<group>"; };
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
@@ -1192,6 +1192,7 @@
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
+		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.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>"; };
@@ -1888,6 +1889,7 @@
 		3833B51E260264AC003021B3 /* Chart */ = {
 			isa = PBXGroup;
 			children = (
+				DDA6E24F2D22187500C2988C /* ChartLegendView.swift */,
 				BD3CC0712B0B89D50013189E /* MainChartView.swift */,
 				BDDAF9F12D0055CC00B34E7A /* ChartElements */,
 			);
@@ -3552,6 +3554,7 @@
 				DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */,
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
+				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
 				DDD6D4D32CDE90720029439A /* HbA1cDisplayUnit.swift in Sources */,
 				38FE826D25CC8461001FF17A /* NightscoutAPI.swift in Sources */,

+ 0 - 1
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -68,7 +68,6 @@ extension Home {
         var totalBolus: Decimal = 0
         var isStatusPopupPresented: Bool = false
         var isLegendPresented: Bool = false
-        var legendSheetDetent = PresentationDetent.large
         var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         var roundedTotalBolus: String = ""
         var selectedTab: Int = 0

+ 229 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ChartLegendView.swift

@@ -0,0 +1,229 @@
+import SwiftUI
+
+struct ChartLegendView: View {
+    @Environment(AppState.self) var appState
+    @Environment(\.colorScheme) var colorScheme
+
+    var state: Home.StateModel
+
+    @State var legendSheetDetent = PresentationDetent.large
+
+    var body: some View {
+        NavigationStack {
+            VStack(alignment: .leading) {
+                Text(
+                    "The main chart in Trio is made up of various elements and shapes. Find their meanings below."
+                )
+                .font(.subheadline)
+                .foregroundColor(.secondary)
+                .padding(.top, 50)
+
+                List {
+                    VStack(alignment: .leading) {
+                        Text("Forecasts").bold().padding(.bottom, 5).textCase(.uppercase)
+                        Text(
+                            "The oref algorithm determines insulin dosing based on a number of scenarios that it estimates with different types of forecasts."
+                        )
+                        .font(.subheadline)
+                        .foregroundColor(.primary)
+
+                        if state.forecastDisplayType == .lines {
+                            legendLinesView
+                        } else {
+                            legendConeOfUncertaintyView
+                        }
+                    }.listRowBackground(Color.gray.opacity(0.1))
+
+                    VStack(alignment: .leading) {
+                        Text("Other Elements & Shapes").bold().padding(.bottom, 5).textCase(.uppercase)
+
+                        DefinitionRow(
+                            term: "Scheduled Basal Rate",
+                            definition: VStack(alignment: .leading, spacing: 10) {
+                                Text("This dotted line represents the hourly insulin rate of your scheduled basal insulin.")
+                                Text("To review or change your scheduled basal rates, go to Settings > Therapy > Basal Rates.")
+                            },
+                            color: Color.insulin,
+                            iconString: "ellipsis"
+                        )
+
+                        DefinitionRow(
+                            term: "Temporary Basal Rate (TBR)",
+                            definition: Text(
+                                "Shows current or past TBRs, which can be set by the oref algorithm or manually."
+                            ),
+                            color: Color.insulin,
+                            iconString: "square"
+                        )
+
+                        DefinitionRow(
+                            term: "Pump Suspension",
+                            definition: Text("Indicates when insulin delivery was paused, i.e. pump is suspended."),
+                            color: Color.loopGray.opacity(colorScheme == .dark ? 0.3 : 0.8),
+                            iconString: "square.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "CGM Glucose Value",
+                            definition: VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Displays real-time glucose readings from your CGM. Depending on your user interface settings, this may be displayed in a static (red, green, orange) or dynamic (full color spectrum) coloring scheme."
+                                )
+                                Text(
+                                    "To modify how glucose readings are displayed, go to Settings > Features > User Interface > Glucose Color Scheme."
+                                )
+                            },
+                            color: Color.green,
+                            iconString: !state.settingsManager.settings.smoothGlucose ? "circle.fill" : "record.circle.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Manual Glucose Measurement",
+                            definition: Text("Manually entered blood glucose, such as a fingerstick test."),
+                            color: Color.red,
+                            iconString: "drop.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Bolus",
+                            definition: Text(
+                                "Shows an insulin dose, which can be a small automated dose (super-micro-bolus), a manually entered dose, or one given externally (e.g., a pen shot)."
+                            ),
+                            color: Color.insulin,
+                            iconString: "arrowtriangle.down.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Carb Entry",
+                            definition: Text("Tracks the carbohydrates you eat, entered to guide insulin dosing."),
+                            color: Color.orange,
+                            iconString: "arrowtriangle.down.fill",
+                            shouldRotateIcon: true
+                        )
+
+                        DefinitionRow(
+                            term: "Fat-Protein Carb Equivalent",
+                            definition: VStack(alignment: .leading, spacing: 10) {
+                                Text(
+                                    "Represents carb equivalent for fat and protein, calculated using the Warsaw Method."
+                                )
+                                Text(
+                                    "To enable or configure Warsaw Method application in Trio, go to Settings > Features > Meal Settings."
+                                )
+                            },
+                            color: Color.brown,
+                            iconString: "circle.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Override",
+                            definition: Text(
+                                "Indicates when an override is or was active, temporarily changing therapy settings (e.g., basal rate, insulin sensitivity, carb ratio, target glucose, or whether Trio can dose SMBs)."
+                            ),
+                            color: Color.purple.opacity(0.4),
+                            iconString: "button.horizontal.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Temporary Target",
+                            definition: Text(
+                                "Marks when a short-term temporary glucose target is or was active, (potentially) altering when or how much insulin is delivered."
+                            ),
+                            color: Color.green.opacity(0.4),
+                            iconString: "button.horizontal.fill"
+                        )
+
+                        DefinitionRow(
+                            term: "Past Insulin-on-Board (IOB)",
+                            definition: Text(
+                                "Shows the IOB value calculated by the algorithm at a specific time in the past. These values are snapshots and won’t change if insulin is added or removed after the fact."
+                            ),
+                            color: Color.darkerBlue.opacity(0.8),
+                            iconString: "line.diagonal"
+                        )
+
+                        DefinitionRow(
+                            term: "Past Carbs-on-Board (COB)",
+                            definition: Text(
+                                "Shows the COB value calculated by the algorithm at a specific time in the past. These values are snapshots and won’t change if carbs are added or removed after the fact."
+                            ),
+                            color: Color.orange.opacity(0.8),
+                            iconString: "line.diagonal"
+                        )
+                    }.listRowBackground(Color.gray.opacity(0.1))
+                }
+                .scrollContentBackground(.hidden)
+                .navigationBarTitle("Chart Legend", displayMode: .inline)
+                .padding(.trailing, 10)
+                .padding(.bottom, 15)
+
+                Button {
+                    state.isLegendPresented.toggle()
+                } label: {
+                    Text("Got it!")
+                        .frame(maxWidth: .infinity, alignment: .center)
+                }
+                .buttonStyle(.bordered)
+                .padding(.top)
+            }
+            .padding([.horizontal, .bottom])
+            .listSectionSpacing(10)
+            .ignoresSafeArea(edges: .top)
+            .presentationDetents(
+                [.fraction(0.9), .large],
+                selection: $legendSheetDetent
+            )
+        }
+    }
+
+    var legendLinesView: some View {
+        Group {
+            DefinitionRow(
+                term: "IOB (Insulin on Board)",
+                definition: Text(
+                    "Forecasts future glucose readings based on the amount of insulin still active in the body."
+                ),
+                color: .insulin
+            )
+
+            DefinitionRow(
+                term: "ZT (Zero-Temp)",
+                definition: Text(
+                    "Forecasts the worst-case future glucose reading scenario if no carbs are absorbed and insulin delivery is stopped until glucose starts rising."
+                ),
+                color: .zt
+            )
+
+            DefinitionRow(
+                term: "COB (Carbs on Board)",
+                definition: Text(
+                    "Forecasts future glucose reading changes by considering the amount of carbohydrates still being absorbed in the body."
+                ),
+                color: .loopYellow
+            )
+
+            DefinitionRow(
+                term: "UAM (Unannounced Meal)",
+                definition: Text(
+                    "Forecasts future glucose levels and insulin dosing needs for unexpected meals or other causes of glucose reading increases without prior notice."
+                ),
+                color: .uam
+            )
+        }
+    }
+
+    var legendConeOfUncertaintyView: some View {
+        DefinitionRow(
+            term: "Cone of Uncertainty",
+            definition: VStack(alignment: .leading, spacing: 10) {
+                Text(
+                    "For simplicity reasons, oref's various forecast curves are displayed as a \"Cone of Uncertainty\" that depicts a possible, forecasted range of future glucose fluctuation based on the current data and the algothim's result."
+                )
+                Text(
+                    "To modify how the forecast is displayed, go to Settings > Features > User Interface > Forecast Display Type."
+                )
+            },
+            color: Color.blue.opacity(0.5)
+        )
+    }
+}

+ 2 - 88
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -898,96 +898,10 @@ extension Home {
                 }
             }
             .sheet(isPresented: $state.isLegendPresented) {
-                legendSheetView()
+                ChartLegendView(state: state)
             }
         }
 
-        @ViewBuilder func legendSheetView() -> some View {
-            NavigationStack {
-                VStack(alignment: .leading, spacing: 16) {
-                    Text(
-                        "The oref algorithm determines insulin dosing based on a number of scenarios that it estimates with different types of forecasts."
-                    )
-                    .font(.subheadline)
-                    .foregroundColor(.secondary)
-
-                    if state.forecastDisplayType == .lines {
-                        legendLinesView()
-                    } else {
-                        legendConeOfUncertaintyView()
-                    }
-
-                    Button {
-                        state.isLegendPresented.toggle()
-                    } label: {
-                        Text("Got it!")
-                            .frame(maxWidth: .infinity, alignment: .center)
-                    }
-                    .buttonStyle(.bordered)
-                    .padding(.top)
-                }
-                .padding()
-                .presentationDetents(
-                    [.fraction(0.9), .large],
-                    selection: $state.legendSheetDetent
-                )
-            }
-        }
-
-        @ViewBuilder func legendLinesView() -> some View {
-            List {
-                DefinitionRow(
-                    term: "IOB (Insulin on Board)",
-                    definition: Text(
-                        "Forecasts future glucose readings based on the amount of insulin still active in the body."
-                    ),
-                    color: .insulin
-                )
-                DefinitionRow(
-                    term: "ZT (Zero-Temp)",
-                    definition: Text(
-                        "Forecasts the worst-case future glucose reading scenario if no carbs are absorbed and insulin delivery is stopped until glucose starts rising."
-                    ),
-                    color: .zt
-                )
-                DefinitionRow(
-                    term: "COB (Carbs on Board)",
-                    definition: Text(
-                        "Forecasts future glucose reading changes by considering the amount of carbohydrates still being absorbed in the body."
-                    ),
-                    color: .loopYellow
-                )
-                DefinitionRow(
-                    term: "UAM (Unannounced Meal)",
-                    definition: Text(
-                        "Forecasts future glucose levels and insulin dosing needs for unexpected meals or other causes of glucose reading increases without prior notice."
-                    ),
-                    color: .uam
-                )
-            }
-            .padding(.trailing, 10)
-            .navigationBarTitle("Legend", displayMode: .inline)
-        }
-
-        @ViewBuilder func legendConeOfUncertaintyView() -> some View {
-            List {
-                DefinitionRow(
-                    term: "Cone of Uncertainty",
-                    definition: VStack {
-                        Text(
-                            "For simplicity reasons, oref's various forecast curves are displayed as a \"Cone of Uncertainty\" that depicts a possible, forecasted range of future glucose fluctuation based on the current data and the algothim's result."
-                        )
-                        Text(
-                            "Note: To modify the forecast display type, go to Trio Settings > Features > User Interface > Forecast Display Type."
-                        )
-                    },
-                    color: Color.blue.opacity(0.5)
-                )
-            }
-            .padding(.trailing, 10)
-            .navigationBarTitle("Legend", displayMode: .inline)
-        }
-
         @ViewBuilder func tabBar() -> some View {
             ZStack(alignment: .bottom) {
                 TabView(selection: $selectedTab) {
@@ -1058,7 +972,7 @@ extension Home {
             }
         }
 
-        //TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
+        // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
         private func parseReasonConclusion(_ reasonConclusion: String, isMmolL _: Bool) -> String {
             let patterns = [
                 "minGuardBG\\s*-?\\d+\\.?\\d*<-?\\d+\\.?\\d*",

+ 2 - 2
FreeAPS/Sources/Services/Network/Nightscout/NightscoutManager.swift

@@ -445,7 +445,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         var modifiedSuggestedDetermination = fetchedSuggestedDetermination
         if var suggestion = fetchedSuggestedDetermination {
             suggestion.timestamp = suggestion.deliverAt
-            
+
             if settingsManager.settings.units == .mmolL {
                 suggestion.reason = parseReasonGlucoseValuesToMmolL(suggestion.reason)
                 // TODO: verify that these parsings are needed for 3rd party apps, e.g., LoopFollow
@@ -1152,7 +1152,7 @@ extension BaseNightscoutManager {
      - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BGI`.
      */
 
-     //TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
+    // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
     func parseReasonGlucoseValuesToMmolL(_ reason: String) -> String {
         let patterns = [
             "ISF:\\s*-?\\d+\\.?\\d*→-?\\d+\\.?\\d*",

+ 26 - 1
FreeAPS/Sources/Views/DefinitionRow.swift

@@ -6,12 +6,37 @@ struct DefinitionRow<DefinitionView: View>: View {
     var definition: DefinitionView
     var color: Color?
     var fontSize: Font?
+    var iconString: String?
+    var shouldRotateIcon: Bool?
+
+    init(
+        term: String,
+        definition: DefinitionView,
+        color: Color? = nil,
+        fontSize: Font? = nil,
+        iconString: String? = nil,
+        shouldRotateIcon: Bool = false
+    ) {
+        self.term = term
+        self.definition = definition
+        self.color = color
+        self.fontSize = fontSize
+        self.iconString = iconString
+        self.shouldRotateIcon = shouldRotateIcon
+    }
 
     var body: some View {
         VStack(alignment: .leading) {
             HStack {
                 if let color = color {
-                    Image(systemName: "circle.fill").foregroundStyle(color)
+                    if let iconString = iconString {
+                        Image(systemName: iconString)
+                            .foregroundStyle(color)
+                            .rotationEffect(shouldRotateIcon == true ? .degrees(180) : .degrees(0))
+                    } else {
+                        Image(systemName: "circle.fill")
+                            .foregroundStyle(color)
+                    }
                 }
                 Text(term).font(fontSize ?? .subheadline).fontWeight(.semibold)
             }.padding(.bottom, 5)

+ 2 - 18
FreeAPS/Sources/Views/SettingInputHintView.swift

@@ -7,24 +7,6 @@ struct SettingInputHintView<HintView: View>: View {
     var hintText: HintView
     var sheetTitle: String
 
-    @Environment(\.colorScheme) private var colorScheme
-    private var color: LinearGradient {
-        colorScheme == .dark ? LinearGradient(
-            gradient: Gradient(colors: [
-                Color.bgDarkBlue,
-                Color.bgDarkerDarkBlue
-            ]),
-            startPoint: .top,
-            endPoint: .bottom
-        )
-            :
-            LinearGradient(
-                gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
-                startPoint: .top,
-                endPoint: .bottom
-            )
-    }
-
     var body: some View {
         NavigationStack {
             List {
@@ -33,7 +15,9 @@ struct SettingInputHintView<HintView: View>: View {
                     definition: hintText,
                     fontSize: .body
                 )
+                .listRowBackground(Color.gray.opacity(0.1))
             }
+            .scrollContentBackground(.hidden)
             .navigationBarTitle(sheetTitle, displayMode: .inline)
 
             Spacer()

+ 1 - 1
FreeAPS/Sources/Views/TagCloudView.swift

@@ -104,7 +104,7 @@ struct TagCloudView: View {
      - Glucose tags handled: `ISF:`, `Target:`, `minPredBG`, `minGuardBG`, `IOBpredBG`, `COBpredBG`, `UAMpredBG`, `Dev:`, `maxDelta`, `BGI`.
      */
 
-     //TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
+    // TODO: Consolidate all mmol parsing methods (in TagCloudView, NightscoutManager and HomeRootView) to one central func
     private func formatGlucoseTags(_ tag: String, isMmolL: Bool) -> String {
         let patterns = [
             "ISF:\\s*-?\\d+\\.?\\d*→-?\\d+\\.?\\d*",