Просмотр исходного кода

Merge pull request #174 from dnzxy/rework-loop-status

Rework Loop Status View (Part 1)
polscm32 1 год назад
Родитель
Сommit
24dffb7c95

+ 10 - 2
FreeAPS.xcodeproj/project.pbxproj

@@ -450,6 +450,7 @@
 		DD1745522C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */; };
 		DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */; };
 		DD1DB7CC2BECCA1F0048B367 /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */; };
+		DD1E53592D273F26008F32A4 /* LoopStatusHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */; };
 		DD21FCB52C6952AD00AF2C25 /* DecimalPickerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */; };
 		DD32CF982CC82463003686D6 /* TrioRemoteControl+Bolus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */; };
 		DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */; };
@@ -478,9 +479,10 @@
 		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 */; };
+		DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E2842D2361F800C2988C /* LoopStatusView.swift */; };
 		DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */; };
 		DDA6E3222D25901100C2988C /* TempTargetHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */; };
-		DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E24F2D22187500C2988C /* ChartLegendView.swift */; };
 		DDA6E3572D25988500C2988C /* ContactImageHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
@@ -1151,6 +1153,7 @@
 		DD1745512C55CA5D00211FAC /* UnitsLimitsSettingsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsStateModel.swift; sourceTree = "<group>"; };
 		DD1745542C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsLimitsSettingsRootView.swift; sourceTree = "<group>"; };
 		DD1DB7CB2BECCA1F0048B367 /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
+		DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusHelpView.swift; sourceTree = "<group>"; };
 		DD21FCB42C6952AD00AF2C25 /* DecimalPickerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalPickerSettings.swift; sourceTree = "<group>"; };
 		DD32CF972CC82460003686D6 /* TrioRemoteControl+Bolus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Bolus.swift"; sourceTree = "<group>"; };
 		DD32CF992CC8246F003686D6 /* TrioRemoteControl+Meal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Meal.swift"; sourceTree = "<group>"; };
@@ -1179,9 +1182,10 @@
 		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>"; };
+		DDA6E2842D2361F800C2988C /* LoopStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStatusView.swift; sourceTree = "<group>"; };
 		DDA6E31F2D258E0500C2988C /* OverrideHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideHelpView.swift; sourceTree = "<group>"; };
 		DDA6E3212D25901100C2988C /* TempTargetHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetHelpView.swift; sourceTree = "<group>"; };
-		DDA6E24F2D22187500C2988C /* ChartLegendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartLegendView.swift; sourceTree = "<group>"; };
 		DDA6E3562D25988500C2988C /* ContactImageHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageHelpView.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>"; };
@@ -1887,6 +1891,8 @@
 		3833B51F260264B6003021B3 /* Header */ = {
 			isa = PBXGroup;
 			children = (
+				DD1E53582D273F20008F32A4 /* LoopStatusHelpView.swift */,
+				DDA6E2842D2361F800C2988C /* LoopStatusView.swift */,
 				383420D525FFE38C002D46C1 /* LoopView.swift */,
 				38AAF85425FFF846004AF583 /* CurrentGlucoseView.swift */,
 				38DAB27F260CBB7F00F74C1A /* PumpView.swift */,
@@ -3522,6 +3528,7 @@
 				DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */,
 				DD1745482C55C61D00211FAC /* AutosensSettingsStateModel.swift in Sources */,
 				DD1745462C55C61500211FAC /* AutosensSettingsProvider.swift in Sources */,
+				DDA6E2852D2361F800C2988C /* LoopStatusView.swift in Sources */,
 				DDA6E3202D258E0500C2988C /* OverrideHelpView.swift in Sources */,
 				DDA6E2502D22187500C2988C /* ChartLegendView.swift in Sources */,
 				3811DEAF25C9D88300A708ED /* KeyValueStorage.swift in Sources */,
@@ -3702,6 +3709,7 @@
 				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
+				DD1E53592D273F26008F32A4 /* LoopStatusHelpView.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
 				195D80BB2AF6980B00D25097 /* DynamicSettingsStateModel.swift in Sources */,
 				E00EEC0327368630002FF094 /* ServiceAssembly.swift in Sources */,

+ 1 - 1
FreeAPS/Sources/Modules/Adjustments/View/Overrides/OverrideHelpView.swift

@@ -38,7 +38,7 @@ struct OverrideHelpView: View {
             .navigationBarTitle("Help", displayMode: .inline)
 
             Button { state.isHelpSheetPresented.toggle() }
-            label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+            label: { Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center) }
                 .buttonStyle(.bordered)
                 .padding(.top)
         }

+ 1 - 1
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/TempTargetHelpView.swift

@@ -25,7 +25,7 @@ struct TempTargetHelpView: View {
             .navigationBarTitle("Help", displayMode: .inline)
 
             Button { state.isHelpSheetPresented.toggle() }
-            label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+            label: { Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center) }
                 .buttonStyle(.bordered)
                 .padding(.top)
         }

+ 1 - 1
FreeAPS/Sources/Modules/ContactImage/View/ContactImageHelpView.swift

@@ -62,7 +62,7 @@ struct ContactImageHelpView: View {
             .navigationBarTitle("Help", displayMode: .inline)
 
             Button { state.isHelpSheetPresented.toggle() }
-            label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+            label: { Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center) }
                 .buttonStyle(.bordered)
                 .padding(.top)
         }

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

@@ -64,7 +64,7 @@ extension Home {
         var timeZone: TimeZone?
         var hours: Int16 = 6
         var totalBolus: Decimal = 0
-        var isStatusPopupPresented: Bool = false
+        var isLoopStatusPresented: Bool = false
         var isLegendPresented: Bool = false
         var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
         var roundedTotalBolus: String = ""

+ 1 - 2
FreeAPS/Sources/Modules/Home/View/Chart/ChartLegendView.swift

@@ -160,8 +160,7 @@ struct ChartLegendView: View {
                 Button {
                     state.isLegendPresented.toggle()
                 } label: {
-                    Text("Got it!")
-                        .frame(maxWidth: .infinity, alignment: .center)
+                    Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
                 }
                 .buttonStyle(.bordered)
                 .padding(.top)

+ 248 - 0
FreeAPS/Sources/Modules/Home/View/Header/LoopStatusHelpView.swift

@@ -0,0 +1,248 @@
+import SwiftUI
+
+struct LoopStatusHelpView: View {
+    @Environment(AppState.self) var appState
+    @Environment(\.colorScheme) var colorScheme
+
+    var state: Home.StateModel
+    var helpSheetDetent: Binding<PresentationDetent>
+    var isHelpSheetPresented: Binding<Bool>
+
+    var body: some View {
+        NavigationStack {
+            VStack(alignment: .leading) {
+                Text(
+                    "The oref algorithm provides recommendations, showing key variables, decisions on temporary basal rates or super-micro-boluses, and a 'reason' field explaining its actions. Find all key terms of this 'reason' explained below:"
+                )
+                .font(.subheadline)
+                .foregroundColor(.secondary)
+                .padding(.top, 50)
+
+                List {
+                    DefinitionRow(
+                        term: "Autosens Ratio",
+                        definition: Text(
+                            "The ratio of how sensitive or resistant to insulin you are in the current loop cycle. Baseline = 1.0, Sensitive < 1.0, Resistant > 1.0"
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "ISF",
+                        definition: Text(
+                            "The first value is your profile Insulin Sensitivity Factor (ISF). The second value, after the arrow, is your adjusted ISF used for the most recent automated dosing calculation."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "COB",
+                        definition: Text(
+                            "Amount of Carbs on Board (COB) used in the most recent automated dosing calculation."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Dev",
+                        definition: Text(
+                            "Abbreviation for 'Deviation'. How much the actual glucose change deviated from the BGI."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "BGI",
+                        definition: Text(
+                            "The degree to which your glucose should be rising or falling based solely on insulin activity."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "CR",
+                        definition: Text(
+                            "The first value is your profile Carb Ratio (CR). The second value, after the arrow, is your adjusted CR used for the most recent automated dosing calculation."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Target",
+                        definition: Text(
+                            "The first value is your target glucose from your settings. The second value, after the arrow, is your adjusted target glucose used for the most recent automated dosing calculation. A second value is shown if you have a temp target, override, or one of the Target Behavior options enabled."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "minPredBG",
+                        definition: Text(
+                            "The lowest forecasted value that Trio has estimated for your future glucose."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "minGuardBG",
+                        definition: Text(
+                            "The lowest forecasted glucose during the remaining duration of insulin action (DIA)."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "IOBpredBG",
+                        definition: Text(
+                            "The forecasted glucose value in 4 hours calculated based on IOB only."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "COBpredBG",
+                        definition: Text(
+                            "The forecasted glucose value in 4 hours calculated based on current IOB and COB."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "UAMpredBG",
+                        definition: Text(
+                            "The forecasted glucose value in 4 hours based on current deviations ramping down to zero at the same rate they have been recently."
+                        ),
+                        color: .insulin
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "TDD",
+                        definition: Text(
+                            "Abbreviation for 'Total Daily Dose'. Last 24 hours of total insulin administered, both basal and bolus."
+                        ),
+                        color: .zt
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Bolus/Basal %",
+                        definition: Text(
+                            "Of the total insulin delivered in the past 24 hours, this indicates what percentage was administered through basals and what was given through bolus."
+                        ),
+                        color: .green
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Dynamic ISF/CR",
+                        definition: Text(
+                            "A display of On/On indicates both Dynamic ISF and CR are enabled. On/Off indicates only Dynamic ISF is enabled. Dynamic CR cannot be enabled when Dynamic ISF is disabled."
+                        ),
+                        color: .zt
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Sigmoid function",
+                        definition: Text("If shown, Sigmoid Dynamic ISF is enabled."),
+                        color: .zt
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Logarithmic formula",
+                        definition: Text("If shown, Logarithmic Dynamic ISF is enabled."),
+                        color: .zt
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "AF",
+                        definition: Text(
+                            "Displays the Adjustment Factor (AF) for either Logathmic or Sigmoid Dynamic ISF in use."
+                        ),
+                        color: .zt
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "SMB Ratio",
+                        definition: Text(
+                            "SMB Delivery Ratio of calculated insulin required that is given as SMB."
+                        ),
+                        color: .zt
+                    ).listRowBackground(Color.gray.opacity(0.1))
+
+                    DefinitionRow(
+                        term: "Smoothing",
+                        definition: Text("Indicates glucose smoothing is enabled."),
+                        color: .gray
+                    ).listRowBackground(Color.gray.opacity(0.1))
+                }
+                .scrollContentBackground(.hidden)
+                .navigationBarTitle("Glossary", displayMode: .inline)
+                .padding(.bottom, 15)
+
+                Button {
+                    isHelpSheetPresented.wrappedValue.toggle()
+                } label: {
+                    Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
+                }
+                .buttonStyle(.bordered)
+                .padding(.top)
+            }
+            .padding([.horizontal, .bottom])
+            .listSectionSpacing(10)
+            .ignoresSafeArea(edges: .top)
+            .presentationDetents(
+                [.fraction(0.9), .large],
+                selection: helpSheetDetent
+            )
+        }
+    }
+
+    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)
+        )
+    }
+}

+ 254 - 0
FreeAPS/Sources/Modules/Home/View/Header/LoopStatusView.swift

@@ -0,0 +1,254 @@
+import SwiftUI
+
+struct LoopStatusView: View {
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    var state: Home.StateModel
+
+    @State var sheetDetent = PresentationDetent.fraction(0.8)
+    // Help Sheet
+    @State var isHelpSheetPresented: Bool = false
+    @State var helpSheetDetent = PresentationDetent.fraction(0.9)
+
+    @State private var statusTitle: String = ""
+
+    var body: some View {
+        NavigationStack {
+            VStack(alignment: .leading, spacing: 10) {
+                Text("Current Loop Status").bold().padding(.top, 20)
+
+                Text(statusTitle)
+                    .font(.headline)
+                    .bold()
+                    .padding(.horizontal, 12)
+                    .padding(.vertical, 6)
+                    .foregroundColor(statusBadgeTextColor)
+                    .background(statusBadgeColor)
+                    .clipShape(Capsule())
+
+                if let errorMessage = state.errorMessage, let date = state.errorDate {
+                    Group {
+                        Text("Error During Algorithm Run at \(Formatter.dateFormatter.string(from: date))").font(.headline)
+                        Text(errorMessage).font(.caption)
+                    }.foregroundColor(.loopRed)
+                }
+
+                if let determination = state.determinationsFromPersistence.first {
+                    if determination.glucose == 400 {
+                        Text("Invalid CGM reading (HIGH).")
+                            .bold()
+                            .padding(.top)
+                            .foregroundStyle(Color.loopRed)
+
+                        Text("SMBs and Non-Zero Temp. Basal Rates are disabled.")
+                            .font(.subheadline)
+
+                    } else {
+                        Text("Latest Raw Algorithm Output")
+                            .bold()
+                            .padding(.top)
+
+                        Text(
+                            "Trio is currently using these metrics and values as determined by the oref algorithm:"
+                        )
+                        .font(.subheadline)
+                        .lineLimit(nil)
+                        .multilineTextAlignment(.leading)
+                        .fixedSize(horizontal: false, vertical: true)
+
+                        let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination
+                            .reasonParts + ["Smoothing: On"]
+                        TagCloudView(
+                            tags: tags,
+                            shouldParseToMmolL: state.units == .mmolL
+                        )
+
+                        Text("Current Algorithm Reasoning").bold().padding(.top)
+
+                        Text(
+                            self
+                                .parseReasonConclusion(
+                                    determination.reasonConclusion,
+                                    isMmolL: state.units == .mmolL
+                                )
+                        )
+                        .font(.subheadline)
+                        .lineLimit(nil)
+                        .multilineTextAlignment(.leading)
+                        .fixedSize(horizontal: false, vertical: true)
+                    }
+                } else {
+                    Text("No recent oref algorithm determination.")
+                }
+
+                Spacer()
+
+                Button {
+                    state.isLoopStatusPresented.toggle()
+                } label: {
+                    Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
+                }
+                .buttonStyle(.bordered)
+                .padding(.top)
+            }
+            .padding(.vertical)
+            .padding(.horizontal, 20)
+            .presentationDetents(
+                [.fraction(0.8), .large],
+                selection: $sheetDetent
+            )
+            .ignoresSafeArea(edges: .top)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(
+                        action: {
+                            isHelpSheetPresented.toggle()
+                        },
+                        label: {
+                            Image(systemName: "questionmark.circle")
+                        }
+                    )
+                }
+            })
+            .onAppear {
+                setStatusTitle()
+            }
+            .sheet(isPresented: $isHelpSheetPresented) {
+                LoopStatusHelpView(state: state, helpSheetDetent: $helpSheetDetent, isHelpSheetPresented: $isHelpSheetPresented)
+            }
+        }
+        .scrollContentBackground(.hidden)
+    }
+
+    private var statusBadgeColor: Color {
+        guard let determination = state.determinationsFromPersistence.first, determination.timestamp != nil
+        else {
+            // previously the .timestamp property was used here because this only gets updated when the reportenacted function in the aps manager gets called
+            return .secondary
+        }
+
+        let delta = state.timerDate.timeIntervalSince(state.lastLoopDate) - 30
+
+        if delta <= 5.minutes.timeInterval {
+            guard determination.timestamp != nil else {
+                return .loopYellow
+            }
+            return .loopGreen
+        } else if delta <= 10.minutes.timeInterval {
+            return .loopYellow
+        } else {
+            return .loopRed
+        }
+    }
+
+    private var statusBadgeTextColor: Color {
+        if statusBadgeColor == .secondary {
+            .black
+        } else {
+            colorScheme == .dark ? Color(red: 25.0 / 255.0, green: 39.0 / 255.0, blue: 53.0 / 255.0, opacity: 1.0) : .white
+        }
+    }
+
+    private func setStatusTitle() {
+        if let determination = state.determinationsFromPersistence.first {
+            statusTitle =
+                "Enacted at \(Formatter.dateFormatter.string(from: determination.deliverAt ?? Date()))"
+        } else {
+            statusTitle = "Not enacted."
+        }
+    }
+
+    // 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*", // minGuardBG x<y
+            "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*", // Eventual BG x >= target
+            "Eventual BG\\s*-?\\d+\\.?\\d*\\s*<\\s*-?\\d+\\.?\\d*", // Eventual BG x < target
+            "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)" // maxDelta x > y% of BG z
+        ]
+        let pattern = patterns.joined(separator: "|")
+        let regex = try! NSRegularExpression(pattern: pattern)
+
+        func convertToMmolL(_ value: String) -> String {
+            if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
+                let mmolValue = Decimal(glucoseValue).asMmolL
+                return mmolValue.description
+            }
+            return value
+        }
+
+        let matches = regex.matches(
+            in: reasonConclusion,
+            range: NSRange(reasonConclusion.startIndex..., in: reasonConclusion)
+        )
+        var updatedConclusion = reasonConclusion
+
+        for match in matches.reversed() {
+            guard let range = Range(match.range, in: reasonConclusion) else { continue }
+            let matchedString = String(reasonConclusion[range])
+
+            if isMmolL {
+                if matchedString.contains("<"), matchedString.contains("Eventual BG"), !matchedString.contains("=") {
+                    // Handle "Eventual BG x < target" pattern
+                    let parts = matchedString.components(separatedBy: "<")
+                    if parts.count == 2 {
+                        let bgPart = parts[0].replacingOccurrences(of: "Eventual BG", with: "")
+                            .trimmingCharacters(in: .whitespaces)
+                        let targetValue = parts[1].trimmingCharacters(in: .whitespaces)
+                        let formattedBGPart = convertToMmolL(bgPart)
+                        let formattedTargetValue = convertToMmolL(targetValue)
+                        let formattedString = "Eventual BG \(formattedBGPart)<\(formattedTargetValue)"
+                        updatedConclusion.replaceSubrange(range, with: formattedString)
+                    }
+                } else if matchedString.contains("<"), matchedString.contains("minGuardBG") {
+                    // Handle "minGuardBG x<y" pattern
+                    let parts = matchedString.components(separatedBy: "<")
+                    if parts.count == 2 {
+                        let firstValue = parts[0].trimmingCharacters(in: .whitespaces)
+                        let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
+                        let formattedFirstValue = convertToMmolL(firstValue)
+                        let formattedSecondValue = convertToMmolL(secondValue)
+                        let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
+                        updatedConclusion.replaceSubrange(range, with: formattedString)
+                    }
+                } else if matchedString.contains(">=") {
+                    // Handle "Eventual BG x >= target" pattern
+                    let parts = matchedString.components(separatedBy: " >= ")
+                    if parts.count == 2 {
+                        let firstValue = parts[0].replacingOccurrences(of: "Eventual BG", with: "")
+                            .trimmingCharacters(in: .whitespaces)
+                        let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
+                        let formattedFirstValue = convertToMmolL(firstValue)
+                        let formattedSecondValue = convertToMmolL(secondValue)
+                        let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
+                        updatedConclusion.replaceSubrange(range, with: formattedString)
+                    }
+                } else if let localMatch = regex.firstMatch(
+                    in: matchedString,
+                    range: NSRange(matchedString.startIndex..., in: matchedString)
+                ) {
+                    // Handle "maxDelta 37 > 20% of BG 95" style
+                    if match.numberOfRanges == 5 {
+                        let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!])
+                        let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!])
+                        let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!])
+                        let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!])
+
+                        let formattedFirstValue = convertToMmolL(firstValue)
+                        let formattedBGValue = convertToMmolL(bgValue)
+
+                        let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)"
+                        updatedConclusion.replaceSubrange(range, with: formattedString)
+                    }
+                }
+            } else {
+                // When isMmolL is false, ensure the original value is retained without duplication
+                updatedConclusion.replaceSubrange(range, with: matchedString)
+            }
+        }
+
+        return updatedConclusion.capitalizingFirstLetter()
+    }
+}

+ 7 - 170
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -33,7 +33,6 @@ extension Home {
         @State var isMenuPresented = false
         @State var showTreatments = false
         @State var selectedTab: Int = 0
-        @State private var statusTitle: String = ""
         @State var showPumpSelection: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
@@ -326,8 +325,7 @@ extension Home {
                     manualTempBasal: state.manualTempBasal,
                     determination: state.determinationsFromPersistence
                 ).onTapGesture {
-                    state.isStatusPopupPresented = true
-                    setStatusTitle()
+                    state.isLoopStatusPresented = true
                 }.onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                     impactHeavy.impactOccurred()
@@ -563,6 +561,8 @@ extension Home {
         }
 
         @ViewBuilder func adjustmentView(geo: GeometryProxy) -> some View {
+//            let background = colorScheme == .dark ? Material.ultraThinMaterial.opacity(0.5) : Color.black.opacity(0.2)
+
             ZStack {
                 /// rectangle as background
                 RoundedRectangle(cornerRadius: 15)
@@ -574,7 +574,7 @@ extension Home {
                                     Color.insulin.opacity(0.1)
                             ) : Color.clear // Use clear and add the Material in the background
                     )
-                    .background(.ultraThinMaterial.opacity(colorScheme == .dark ? 0.35 : 0))
+                    .background(colorScheme == .dark ? Color.chart.opacity(0.25) : Color.black.opacity(0.075))
                     .clipShape(RoundedRectangle(cornerRadius: 15))
                     .frame(height: geo.size.height * 0.08)
                     .shadow(
@@ -854,26 +854,9 @@ extension Home {
             .navigationTitle("Home")
             .navigationBarHidden(true)
             .ignoresSafeArea(.keyboard)
-            .popup(isPresented: state.isStatusPopupPresented, alignment: .top, direction: .top) {
-                popup
-                    .padding()
-                    .background(
-                        RoundedRectangle(cornerRadius: 8, style: .continuous)
-                            .fill(colorScheme == .dark ? Color(
-                                "Chart"
-                            ) : Color(UIColor.darkGray))
-                    )
-                    .onTapGesture {
-                        state.isStatusPopupPresented = false
-                    }
-                    .gesture(
-                        DragGesture(minimumDistance: 10, coordinateSpace: .local)
-                            .onEnded { value in
-                                if value.translation.height < 0 {
-                                    state.isStatusPopupPresented = false
-                                }
-                            }
-                    )
+            .blur(radius: state.isLoopStatusPresented ? 3 : 0)
+            .sheet(isPresented: $state.isLoopStatusPresented) {
+                LoopStatusView(state: state)
             }
             .confirmationDialog("Pump Model", isPresented: $showPumpSelection) {
                 Button("Medtronic") { state.addPump(.minimed) }
@@ -975,152 +958,6 @@ extension Home {
                 }
             }
         }
-
-        // 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*", // minGuardBG x<y
-                "Eventual BG\\s*-?\\d+\\.?\\d*\\s*>=\\s*-?\\d+\\.?\\d*", // Eventual BG x >= target
-                "Eventual BG\\s*-?\\d+\\.?\\d*\\s*<\\s*-?\\d+\\.?\\d*", // Eventual BG x < target
-                "(\\S+)\\s+(-?\\d+\\.?\\d*)\\s*>\\s*(\\d+)%\\s+of\\s+BG\\s+(-?\\d+\\.?\\d*)" // maxDelta x > y% of BG z
-            ]
-            let pattern = patterns.joined(separator: "|")
-            let regex = try! NSRegularExpression(pattern: pattern)
-
-            func convertToMmolL(_ value: String) -> String {
-                if let glucoseValue = Double(value.replacingOccurrences(of: "[^\\d.-]", with: "", options: .regularExpression)) {
-                    let mmolValue = Decimal(glucoseValue).asMmolL
-                    return mmolValue.description
-                }
-                return value
-            }
-
-            let matches = regex.matches(
-                in: reasonConclusion,
-                range: NSRange(reasonConclusion.startIndex..., in: reasonConclusion)
-            )
-            var updatedConclusion = reasonConclusion
-
-            for match in matches.reversed() {
-                guard let range = Range(match.range, in: reasonConclusion) else { continue }
-                let matchedString = String(reasonConclusion[range])
-
-                if isMmolL {
-                    if matchedString.contains("<"), matchedString.contains("Eventual BG"), !matchedString.contains("=") {
-                        // Handle "Eventual BG x < target" pattern
-                        let parts = matchedString.components(separatedBy: "<")
-                        if parts.count == 2 {
-                            let bgPart = parts[0].replacingOccurrences(of: "Eventual BG", with: "")
-                                .trimmingCharacters(in: .whitespaces)
-                            let targetValue = parts[1].trimmingCharacters(in: .whitespaces)
-                            let formattedBGPart = convertToMmolL(bgPart)
-                            let formattedTargetValue = convertToMmolL(targetValue)
-                            let formattedString = "Eventual BG \(formattedBGPart)<\(formattedTargetValue)"
-                            updatedConclusion.replaceSubrange(range, with: formattedString)
-                        }
-                    } else if matchedString.contains("<"), matchedString.contains("minGuardBG") {
-                        // Handle "minGuardBG x<y" pattern
-                        let parts = matchedString.components(separatedBy: "<")
-                        if parts.count == 2 {
-                            let firstValue = parts[0].trimmingCharacters(in: .whitespaces)
-                            let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
-                            let formattedFirstValue = convertToMmolL(firstValue)
-                            let formattedSecondValue = convertToMmolL(secondValue)
-                            let formattedString = "minGuardBG \(formattedFirstValue)<\(formattedSecondValue)"
-                            updatedConclusion.replaceSubrange(range, with: formattedString)
-                        }
-                    } else if matchedString.contains(">=") {
-                        // Handle "Eventual BG x >= target" pattern
-                        let parts = matchedString.components(separatedBy: " >= ")
-                        if parts.count == 2 {
-                            let firstValue = parts[0].replacingOccurrences(of: "Eventual BG", with: "")
-                                .trimmingCharacters(in: .whitespaces)
-                            let secondValue = parts[1].trimmingCharacters(in: .whitespaces)
-                            let formattedFirstValue = convertToMmolL(firstValue)
-                            let formattedSecondValue = convertToMmolL(secondValue)
-                            let formattedString = "Eventual BG \(formattedFirstValue) >= \(formattedSecondValue)"
-                            updatedConclusion.replaceSubrange(range, with: formattedString)
-                        }
-                    } else if let localMatch = regex.firstMatch(
-                        in: matchedString,
-                        range: NSRange(matchedString.startIndex..., in: matchedString)
-                    ) {
-                        // Handle "maxDelta 37 > 20% of BG 95" style
-                        if match.numberOfRanges == 5 {
-                            let metric = String(matchedString[Range(localMatch.range(at: 1), in: matchedString)!])
-                            let firstValue = String(matchedString[Range(localMatch.range(at: 2), in: matchedString)!])
-                            let percentage = String(matchedString[Range(localMatch.range(at: 3), in: matchedString)!])
-                            let bgValue = String(matchedString[Range(localMatch.range(at: 4), in: matchedString)!])
-
-                            let formattedFirstValue = convertToMmolL(firstValue)
-                            let formattedBGValue = convertToMmolL(bgValue)
-
-                            let formattedString = "\(metric) \(formattedFirstValue) > \(percentage)% of BG \(formattedBGValue)"
-                            updatedConclusion.replaceSubrange(range, with: formattedString)
-                        }
-                    }
-                } else {
-                    // When isMmolL is false, ensure the original value is retained without duplication
-                    updatedConclusion.replaceSubrange(range, with: matchedString)
-                }
-            }
-
-            return updatedConclusion.capitalizingFirstLetter()
-        }
-
-        private var popup: some View {
-            VStack(alignment: .leading, spacing: 4) {
-                Text(statusTitle).font(.headline).foregroundColor(.white)
-                    .padding(.bottom, 4)
-                if let determination = state.determinationsFromPersistence.first {
-                    if determination.glucose == 400 {
-                        Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed).padding(.top, 8)
-                        Text("SMBs and High Temps Disabled.").font(.caption).foregroundColor(.white).padding(.bottom, 4)
-                    } else {
-                        let tags = !state.isSmoothingEnabled ? determination.reasonParts : determination
-                            .reasonParts + ["Smoothing: On"]
-                        TagCloudView(
-                            tags: tags,
-                            shouldParseToMmolL: state.units == .mmolL
-                        )
-                        .animation(.none, value: false)
-
-                        Text(
-                            self
-                                .parseReasonConclusion(
-                                    determination.reasonConclusion,
-                                    isMmolL: state.units == .mmolL
-                                )
-                        ).font(.caption).foregroundColor(.white)
-                    }
-                } else {
-                    Text("No determination found").font(.body).foregroundColor(.white)
-                }
-
-                if let errorMessage = state.errorMessage, let date = state.errorDate {
-                    Text(NSLocalizedString("Error at", comment: "") + " " + Formatter.dateFormatter.string(from: date))
-                        .foregroundColor(.white)
-                        .font(.headline)
-                        .padding(.bottom, 4)
-                        .padding(.top, 8)
-                    Text(errorMessage).font(.caption).foregroundColor(.loopRed)
-                }
-            }
-        }
-
-        private func setStatusTitle() {
-            if let determination = state.determinationsFromPersistence.first {
-                let dateFormatter = DateFormatter()
-                dateFormatter.timeStyle = .short
-                statusTitle = NSLocalizedString("Oref Determination enacted at", comment: "Headline in enacted pop up") +
-                    " " +
-                    dateFormatter
-                    .string(from: determination.deliverAt ?? Date())
-            } else {
-                statusTitle = "No Oref determination"
-                return
-            }
-        }
     }
 }
 

+ 1 - 1
FreeAPS/Sources/Modules/Treatments/View/PopupView.swift

@@ -101,7 +101,7 @@ struct PopupView: View {
                 Spacer()
 
                 Button { state.showInfo = false }
-                label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                label: { Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center) }
                     .buttonStyle(.bordered)
                     .padding(.top)
             }

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

@@ -25,8 +25,7 @@ struct SettingInputHintView<HintView: View>: View {
             Button {
                 shouldDisplayHint.toggle()
             } label: {
-                Text("Got it!")
-                    .frame(maxWidth: .infinity, alignment: .center)
+                Text("Got it!").bold().frame(maxWidth: .infinity, minHeight: 30, alignment: .center)
             }
             .buttonStyle(.bordered)
             .padding(.top)

+ 25 - 16
FreeAPS/Sources/Views/TagCloudView.swift

@@ -7,7 +7,10 @@ struct TagCloudView: View {
     var tags: [String]
     var shouldParseToMmolL: Bool
 
-    @State private var totalHeight = CGFloat.infinity // << variant for VStack
+    @Environment(\.colorScheme) var colorScheme
+
+    @State private var totalHeight = CGFloat.zero // << variant for ScrollView/List
+//    = CGFloat.infinity // << variant for VStack
 
     var body: some View {
         VStack {
@@ -15,27 +18,28 @@ struct TagCloudView: View {
                 self.generateContent(in: geometry)
             }
         }
-        .frame(maxHeight: totalHeight) // << variant for VStack
+        .frame(height: totalHeight) // << variant for ScrollView/List
+//        .frame(maxHeight: totalHeight) // << variant for VStack
     }
 
-    private func generateContent(in g: GeometryProxy) -> some View {
+    private func generateContent(in geometry: GeometryProxy) -> some View {
         var width = CGFloat.zero
         var height = CGFloat.zero
 
         return ZStack(alignment: .topLeading) {
             ForEach(self.tags, id: \.self) { tag in
-                self.item(for: tag, isMmolL: shouldParseToMmolL)
-                    .padding([.horizontal, .vertical], 2)
-                    .alignmentGuide(.leading, computeValue: { d in
-                        if abs(width - d.width) > g.size.width {
+                self.drawTag(for: tag, isMmolL: shouldParseToMmolL)
+                    .padding([.horizontal, .vertical], 3)
+                    .alignmentGuide(.leading, computeValue: { dimensions in
+                        if abs(width - dimensions.width) > geometry.size.width {
                             width = 0
-                            height -= d.height
+                            height -= dimensions.height
                         }
                         let result = width
                         if tag == self.tags.last! {
                             width = 0 // last item
                         } else {
-                            width -= d.width
+                            width -= dimensions.width
                         }
                         return result
                     })
@@ -50,7 +54,7 @@ struct TagCloudView: View {
         }.background(viewHeightReader($totalHeight))
     }
 
-    private func item(for textTag: String, isMmolL: Bool) -> some View {
+    private func drawTag(for textTag: String, isMmolL: Bool) -> some View {
         var colorOfTag: Color {
             switch textTag {
             case textTag where textTag.contains("SMB Delivery Ratio:"):
@@ -72,7 +76,7 @@ struct TagCloudView: View {
             case textTag where textTag.contains("SMB Ratio"):
                 return .orange
             case textTag where textTag.contains("Smoothing: On"):
-                return .white
+                return .gray
             default:
                 return .insulin
             }
@@ -82,12 +86,17 @@ struct TagCloudView: View {
 
         return ZStack {
             Text(formattedTextTag)
-                .padding(.vertical, 2)
-                .padding(.horizontal, 4)
+                .padding(.horizontal, 10)
+                .padding(.vertical, 5)
                 .font(.subheadline)
-                .background(colorOfTag.opacity(0.8))
-                .foregroundColor(textTag.contains("Smoothing: On") ? Color.black : Color.white)
-                .cornerRadius(2)
+                .fontWeight(.semibold)
+                .background(colorOfTag.opacity(colorScheme == .dark ? 0.15 : 0.25))
+                .foregroundColor(colorOfTag)
+                .clipShape(Capsule())
+                .overlay(
+                    Capsule()
+                        .stroke(colorOfTag.opacity(0.4), lineWidth: 2)
+                )
         }
     }