Sfoglia il codice sorgente

Full-tap panels, contained bar, stats panel faces

Whole adjustment panel and stats banner respond to taps, not just
their text; cancel buttons keep precedence. Remaining-time bar is
inset so it stays inside the rounded corners. New homeStatsPanelFace
setting: time in range (default), distribution bar only, or today's
average + GMI.
dnzxy 4 giorni fa
parent
commit
d7b898ff84

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -866,6 +866,7 @@
 		DDE179702C910127003CDDB7 /* OverrideStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */; };
 		DDE179712C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */; };
 		DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */; };
+		7A11AC0DE000000000000020 /* HomeStatsPanelFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11AC0DE000000000000120 /* HomeStatsPanelFace.swift */; };
 		DDF68FFC2D9ECF7F008BF16C /* OnboardingStateModel+Nightscout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */; };
 		DDF6902C2DA028D3008BF16C /* DiagnosticsStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */; };
 		DDF6905C2DA0AFC5008BF16C /* WelcomeStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */; };
@@ -1886,6 +1887,7 @@
 		DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInRangeType.swift; sourceTree = "<group>"; };
+		7A11AC0DE000000000000120 /* HomeStatsPanelFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeStatsPanelFace.swift; sourceTree = "<group>"; };
 		DDF68FFB2D9ECF77008BF16C /* OnboardingStateModel+Nightscout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingStateModel+Nightscout.swift"; sourceTree = "<group>"; };
 		DDF6902B2DA028D3008BF16C /* DiagnosticsStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsStepView.swift; sourceTree = "<group>"; };
 		DDF6905B2DA0AFC5008BF16C /* WelcomeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStepView.swift; sourceTree = "<group>"; };
@@ -2787,6 +2789,7 @@
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				BD54A9722D281A9C00F9C1EE /* TempTargetPresetWatch.swift */,
 				DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
+				7A11AC0DE000000000000120 /* HomeStatsPanelFace.swift */,
 				1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */,
 				38AEE73C25F0200C0013F05B /* TrioSettings.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
@@ -5461,6 +5464,7 @@
 				BD47FD172D88AAF50043966B /* CompletedStepView.swift in Sources */,
 				DD4AFFF12DADB59100AB7387 /* AlgorithmSettingsContentsStepView.swift in Sources */,
 				DDEBB05C2D89E9050032305D /* TimeInRangeType.swift in Sources */,
+				7A11AC0DE000000000000020 /* HomeStatsPanelFace.swift in Sources */,
 				DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */,
 				BD47FDDB2D8B659B0043966B /* BasalProfileStepView.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,

+ 19 - 0
Trio/Sources/Models/HomeStatsPanelFace.swift

@@ -0,0 +1,19 @@
+import Foundation
+
+enum HomeStatsPanelFace: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case timeInRange
+    case distributionBar
+    case averages
+
+    var displayName: String {
+        switch self {
+        case .timeInRange:
+            return String(localized: "Time in Range", comment: "Home stats panel face option")
+        case .distributionBar:
+            return String(localized: "Distribution Bar Only", comment: "Home stats panel face option")
+        case .averages:
+            return String(localized: "Today's Averages", comment: "Home stats panel face option")
+        }
+    }
+}

+ 5 - 0
Trio/Sources/Models/TrioSettings.swift

@@ -69,6 +69,7 @@ struct TrioSettings: JSON, Equatable, Encodable {
     var displayGlucoseForecasts: Bool = false
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var timeInRangeType: TimeInRangeType = .timeInTightRange
+    var homeStatsPanelFace: HomeStatsPanelFace = .timeInRange
     var requireAdjustmentsConfirmation: Bool = false
 
     /// Selected Garmin watchface (Trio or SwissAlpine)
@@ -323,6 +324,10 @@ extension TrioSettings: Decodable {
             settings.timeInRangeType = timeInRangeType
         }
 
+        if let homeStatsPanelFace = try? container.decode(HomeStatsPanelFace.self, forKey: .homeStatsPanelFace) {
+            settings.homeStatsPanelFace = homeStatsPanelFace
+        }
+
         if let requireAdjustmentsConfirmation = try? container.decode(Bool.self, forKey: .requireAdjustmentsConfirmation) {
             settings.requireAdjustmentsConfirmation = requireAdjustmentsConfirmation
         }

+ 79 - 26
Trio/Sources/Modules/Home/View/HomeRootView+BottomControls.swift

@@ -207,9 +207,6 @@ extension Home.RootView {
                     .foregroundStyle(.secondary)
             }
         }
-        .onTapGesture {
-            selectedTab = 2
-        }
     }
 
     @ViewBuilder func adjustmentsTempTargetView(_ tempTargetString: String) -> some View {
@@ -223,9 +220,6 @@ extension Home.RootView {
                     .foregroundStyle(.secondary)
             }
         }
-        .onTapGesture {
-            selectedTab = 2
-        }
     }
 
     @ViewBuilder func adjustmentsCancelView(_ cancelAction: @escaping () -> Void) -> some View {
@@ -305,8 +299,6 @@ extension Home.RootView {
             Image(systemName: "xmark.app")
                 .font(.title)
                 .foregroundStyle(Color.clear)
-        }.onTapGesture {
-            selectedTab = 2
         }
     }
 
@@ -359,16 +351,19 @@ extension Home.RootView {
                 )
                 .frame(height: HomeLayout.bottomPanelHeight)
                 .overlay(alignment: .bottom) {
-                    if isConcurrent {
-                        HStack(spacing: 6) {
-                            remainingBar(overrideRemainingFraction, tint: .purple)
-                            remainingBar(tempTargetRemainingFraction, tint: .loopGreen)
+                    // inset clears the corner curve so the bar stays inside the shape
+                    Group {
+                        if isConcurrent {
+                            HStack(spacing: 6) {
+                                remainingBar(overrideRemainingFraction, tint: .purple)
+                                remainingBar(tempTargetRemainingFraction, tint: .loopGreen)
+                            }
+                        } else if let tint = tint {
+                            remainingBar(overrideRemainingFraction ?? tempTargetRemainingFraction, tint: tint)
                         }
-                        .padding(.horizontal, 4)
-                    } else if let tint = tint {
-                        remainingBar(overrideRemainingFraction ?? tempTargetRemainingFraction, tint: tint)
-                            .padding(.horizontal, 4)
                     }
+                    .padding(.horizontal, 14)
+                    .padding(.bottom, 3)
                 }
                 .clipShape(RoundedRectangle(cornerRadius: 17, style: .continuous))
                 .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.10), radius: 3, y: 1)
@@ -440,7 +435,13 @@ extension Home.RootView {
                 } message: {
                     Text("Select Adjustment")
                 }
-        }.padding(.horizontal, 10)
+        }
+        // whole panel navigates; the cancel buttons' own gestures take precedence
+        .contentShape(Rectangle())
+        .onTapGesture {
+            selectedTab = 2
+        }
+        .padding(.horizontal, 10)
     }
 
     @ViewBuilder func bolusView(_ progress: Decimal) -> some View {
@@ -531,7 +532,37 @@ extension Home.RootView {
         }
     }
 
+    /// Mean glucose (mg/dL) of today's readings, nil without data.
+    private var todayMeanGlucose: Double? {
+        let startOfDay = Calendar.current.startOfDay(for: Date())
+        let values = state.glucoseFromPersistence
+            .filter { ($0.date ?? .distantPast) >= startOfDay }
+            .map { Double($0.glucose) }
+        guard !values.isEmpty else { return nil }
+        return values.reduce(0, +) / Double(values.count)
+    }
+
+    private var todayAverageString: String {
+        guard let mean = todayMeanGlucose else { return "--" }
+        if state.units == .mmolL {
+            return Decimal(mean).asMmolL.formatted(.number.precision(.fractionLength(1))) + " " + GlucoseUnits.mmolL.rawValue
+        }
+        return "\(Int(mean.rounded())) " + GlucoseUnits.mgdL.rawValue
+    }
+
+    private var todayGMIString: String {
+        guard let mean = todayMeanGlucose else { return "--" }
+        let gmiPercentage = 3.31 + 0.02392 * mean
+        // settingsManager is injected after first render; default until then
+        if state.settingsManager?.settings.eA1cDisplayUnit == .mmolMol {
+            let gmiMmolMol = (gmiPercentage - 2.152) * 10.929
+            return "\(Int(gmiMmolMol.rounded())) mmol/mol"
+        }
+        return gmiPercentage.formatted(.number.precision(.fractionLength(1))) + " %"
+    }
+
     @ViewBuilder func statsBanner() -> some View {
+        let face = state.settingsManager?.settings.homeStatsPanelFace ?? .timeInRange
         let distribution = state.todayGlucoseDistribution
         let coveragePct = distribution.veryLowPct + distribution.lowPct + distribution.inRangePct + distribution
             .highPct + distribution.veryHighPct
@@ -565,18 +596,39 @@ extension Home.RootView {
                     .shadow(color: Color.black.opacity(colorScheme == .dark ? 0.25 : 0.10), radius: 3, y: 1)
 
                 HStack(alignment: .center, spacing: 12) {
-                    VStack(alignment: .leading, spacing: 4) {
-                        HStack(alignment: .firstTextBaseline, spacing: 6) {
-                            Text(tirString)
-                                .font(.title2).fontWeight(.bold).fontDesign(.rounded)
-                                .foregroundStyle(.primary)
+                    switch face {
+                    case .timeInRange:
+                        VStack(alignment: .leading, spacing: 4) {
+                            HStack(alignment: .firstTextBaseline, spacing: 6) {
+                                Text(tirString)
+                                    .font(.title2).fontWeight(.bold).fontDesign(.rounded)
+                                    .foregroundStyle(.primary)
+                                Text("Time in Range", comment: "Stats banner subtitle")
+                                    .font(.subheadline)
+                                    .foregroundStyle(.secondary)
+                            }
+
+                            statsDistributionBar(segments)
+                                .frame(height: 6)
+                        }
+                    case .distributionBar:
+                        VStack(alignment: .leading, spacing: 6) {
                             Text("Time in Range", comment: "Stats banner subtitle")
-                                .font(.subheadline)
+                                .font(.subheadline).fontWeight(.semibold)
                                 .foregroundStyle(.secondary)
-                        }
 
-                        statsDistributionBar(segments)
-                            .frame(height: 6)
+                            statsDistributionBar(segments)
+                                .frame(height: 6)
+                        }
+                    case .averages:
+                        VStack(alignment: .leading, spacing: 1) {
+                            Text("\u{2300} \(todayAverageString) \u{00B7} GMI \(todayGMIString)")
+                                .font(.subheadline).fontWeight(.semibold)
+                                .foregroundStyle(.primary)
+                            Text("Today's average", comment: "Stats banner subtitle")
+                                .font(.caption)
+                                .foregroundStyle(.secondary)
+                        }
                     }
 
                     Spacer(minLength: 8)
@@ -588,6 +640,7 @@ extension Home.RootView {
                 .padding(.horizontal, 16)
             }
             .padding(.horizontal, 10)
+            .contentShape(Rectangle())
         }
         .buttonStyle(.plain)
     }

+ 2 - 0
Trio/Sources/Modules/UserInterfaceSettings/UserInterfaceSettingsStateModel.swift

@@ -14,6 +14,7 @@ extension UserInterfaceSettings {
         @Published var glucoseColorScheme: GlucoseColorScheme = .staticColor
         @Published var eA1cDisplayUnit: EstimatedA1cDisplayUnit = .percent
         @Published var timeInRangeType: TimeInRangeType = .timeInTightRange
+        @Published var homeStatsPanelFace: HomeStatsPanelFace = .timeInRange
         @Published var requireAdjustmentsConfirmation: Bool = false
 
         var units: GlucoseUnits = .mgdL
@@ -45,6 +46,7 @@ extension UserInterfaceSettings {
             subscribeSetting(\.eA1cDisplayUnit, on: $eA1cDisplayUnit) { eA1cDisplayUnit = $0 }
 
             subscribeSetting(\.timeInRangeType, on: $timeInRangeType) { timeInRangeType = $0 }
+            subscribeSetting(\.homeStatsPanelFace, on: $homeStatsPanelFace) { homeStatsPanelFace = $0 }
 
             subscribeSetting(\.requireAdjustmentsConfirmation, on: $requireAdjustmentsConfirmation) {
                 requireAdjustmentsConfirmation = $0 }

+ 40 - 0
Trio/Sources/Modules/UserInterfaceSettings/View/UserInterfaceSettingsRootView.swift

@@ -543,6 +543,46 @@ extension UserInterfaceSettings {
                     }.padding(.bottom)
                 }.settingsSearchTarget(label: String(localized: "Time in Range Type"))
 
+                Section {
+                    VStack(alignment: .leading) {
+                        Picker(
+                            selection: $state.homeStatsPanelFace,
+                            label: Text("Home Statistics Panel").multilineTextAlignment(.leading)
+                        ) {
+                            ForEach(HomeStatsPanelFace.allCases) { selection in
+                                Text(selection.displayName).tag(selection)
+                            }
+                        }.padding(.top)
+
+                        HStack(alignment: .center) {
+                            Text(
+                                "Choose what the statistics panel on the home screen displays."
+                            )
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .lineLimit(nil)
+                            Spacer()
+                            Button(
+                                action: {
+                                    hintLabel = String(localized: "Home Statistics Panel")
+                                    selectedVerboseHint =
+                                        AnyView(
+                                            Text(
+                                                "Choose what the statistics panel on the home screen shows by default. Tapping the panel always opens the full statistics view.\n\nTime in Range: today's time in range percentage with a glucose distribution bar.\n\nDistribution Bar Only: just the glucose distribution bar, without the percentage.\n\nToday's Averages: today's average glucose and GMI (Glucose Management Index)."
+                                            )
+                                        )
+                                    shouldDisplayHint.toggle()
+                                },
+                                label: {
+                                    HStack {
+                                        Image(systemName: "questionmark.circle")
+                                    }
+                                }
+                            ).buttonStyle(BorderlessButtonStyle())
+                        }.padding(.top)
+                    }.padding(.bottom)
+                }.settingsSearchTarget(label: String(localized: "Home Statistics Panel"))
+
                 SettingInputSection(
                     decimalValue: $state.carbsRequiredThreshold,
                     booleanValue: $state.showCarbsRequiredBadge,