瀏覽代碼

Fix G5/G6 sensor-expiry math and home tag layout

- HomeStateModel: read Glucose.sessionExpDate directly for G5/G6.
  Reverse-deriving from activationDate produced "7337d remaining" on
  Anubis-modded transmitters whose activationDate is the months-old
  pairing time rather than session start.
- PluginSource: publish cgmStatusHighlight / cgmLifecycleProgress from
  cgmManager(_:hasNew:) so cold-start picks them up instead of waiting
  for a config-level state update.
- CurrentGlucoseView: tag floats in a .overlay so it no longer pushes
  chart / stats / tab bar down, and hides when the trend triangle
  rotates into the lower half of the bobble.
- SensorStatusTagView: padding tweaked for the new positioning.
Deniz Cengiz 3 周之前
父節點
當前提交
f39e9f4994

+ 7 - 0
Trio/Sources/APS/CGM/PluginSource.swift

@@ -140,6 +140,13 @@ extension PluginSource: CGMManagerDelegate {
                 debug(.deviceManager, "CGM PLUGIN - unable to read CGM result")
                 debug(.deviceManager, "CGM PLUGIN - unable to read CGM result")
             }
             }
 
 
+            // New reading means `latestReading` (and thus lifecycle /
+            // highlight) advanced — republish so the home arc + tag pick
+            // it up. `cgmManagerDidUpdateState` only fires on config-level
+            // changes, which means cold-start (no reading yet) would leave
+            // the subjects stuck at nil until something else triggered.
+            self.publishCGMStatus()
+
             debug(.deviceManager, "CGM PLUGIN - Direct return done")
             debug(.deviceManager, "CGM PLUGIN - Direct return done")
         }
         }
     }
     }

+ 12 - 9
Trio/Sources/Modules/Home/HomeStateModel.swift

@@ -673,11 +673,16 @@ extension Home {
         /// in-memory sensor state.
         /// in-memory sensor state.
         ///
         ///
         /// Falls back to `GlucoseSimulatorSource`'s synthetic values when no
         /// Falls back to `GlucoseSimulatorSource`'s synthetic values when no
-        /// real `CGMManagerUI` is active — lets the lifecycle indicator be
-        /// exercised end-to-end without Libre/Dexcom hardware.
-        /// G7 exposes `sensorExpiresAt` directly; for everyone else we reverse-
-        /// derive it from `activatedAt + percentComplete`. Falls through to
-        /// nil if neither is available — UI then drops the time label.
+        /// Best available `sensorExpiresAt` for the home label:
+        /// 1. Some managers expose it directly (G7 `sensorExpiresAt`, G5/G6
+        ///    `latestReading.sessionExpDate`).
+        /// 2. Others only expose `activatedAt` — combined with
+        ///    `lifecycle.percentComplete` we reverse-derive total lifetime.
+        ///    Critically `activatedAt` must be the *session* start, not the
+        ///    transmitter activation date, or the math blows up (an Anubis-
+        ///    modded G6 with `Glucose.activationDate` months old produced
+        ///    "7337d remaining" — bug, fixed by reading session start instead).
+        /// 3. Falls through to nil — UI drops the time label.
         private static func resolveSensorExpiresAt(
         private static func resolveSensorExpiresAt(
             manager: CGMManagerUI?,
             manager: CGMManagerUI?,
             glucoseSource: GlucoseSource?,
             glucoseSource: GlucoseSource?,
@@ -688,14 +693,12 @@ extension Home {
             }
             }
             guard let manager else { return nil }
             guard let manager else { return nil }
             if let g7 = manager as? G7CGMManager, let exp = g7.sensorExpiresAt { return exp }
             if let g7 = manager as? G7CGMManager, let exp = g7.sensorExpiresAt { return exp }
+            if let g6 = manager as? G6CGMManager, let exp = g6.latestReading?.sessionExpDate { return exp }
+            if let g5 = manager as? G5CGMManager, let exp = g5.latestReading?.sessionExpDate { return exp }
 
 
             let activatedAt: Date?
             let activatedAt: Date?
             if let g7 = manager as? G7CGMManager {
             if let g7 = manager as? G7CGMManager {
                 activatedAt = g7.sensorActivatedAt
                 activatedAt = g7.sensorActivatedAt
-            } else if let g6 = manager as? G6CGMManager {
-                activatedAt = g6.latestReading?.activationDate ?? g6.latestReading?.sessionStartDate
-            } else if let g5 = manager as? G5CGMManager {
-                activatedAt = g5.latestReading?.activationDate ?? g5.latestReading?.sessionStartDate
             } else if let libre = manager as? LibreTransmitterManagerV3 {
             } else if let libre = manager as? LibreTransmitterManagerV3 {
                 activatedAt = libre.sensorInfoObservable.activatedAt
                 activatedAt = libre.sensorInfoObservable.activatedAt
             } else {
             } else {

+ 51 - 41
Trio/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -51,54 +51,59 @@ struct CurrentGlucoseView: View {
         let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
         let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
 
 
         if cgmAvailable {
         if cgmAvailable {
-            VStack(spacing: 0) {
-                ZStack {
-                    if let progress = cgmProgress {
-                        SensorLifecycleArcView(
-                            progress: progress.percentComplete,
-                            progressState: progress.progressState
-                        )
-                    }
+            ZStack {
+                if let progress = cgmProgress {
+                    SensorLifecycleArcView(
+                        progress: progress.percentComplete,
+                        progressState: progress.progressState
+                    )
+                }
 
 
-                    TrendShape(gradient: angularGradient, color: triangleColor, showArrow: true)
-                        .rotationEffect(.degrees(rotationDegrees))
+                TrendShape(gradient: angularGradient, color: triangleColor, showArrow: true)
+                    .rotationEffect(.degrees(rotationDegrees))
 
 
-                    VStack(alignment: .center) {
-                        bobbleContent()
-                    }
-                }
-                .onChange(of: glucose.last?.directionEnum) {
-                    withAnimation {
-                        switch glucose.last?.directionEnum {
-                        case .doubleUp,
-                             .singleUp,
-                             .tripleUp:
-                            rotationDegrees = -90
-                        case .fortyFiveUp:
-                            rotationDegrees = -45
-                        case .flat:
-                            rotationDegrees = 0
-                        case .fortyFiveDown:
-                            rotationDegrees = 45
-                        case .doubleDown,
-                             .singleDown,
-                             .tripleDown:
-                            rotationDegrees = 90
-                        case nil,
-                             .notComputable,
-                             .rateOutOfRange:
-                            rotationDegrees = 0
-                        default:
-                            rotationDegrees = 0
-                        }
-                    }
+                VStack(alignment: .center) {
+                    bobbleContent()
                 }
                 }
-
-                if let tag = tagLabel {
+            }
+            .overlay(alignment: .bottom) {
+                // Tag floats outside the bobble's frame so it doesn't push
+                // chart / stats / tab bar down. When the trend triangle
+                // rotates into the lower half (`rotationDegrees >= 45`) it
+                // ends up around 6 o'clock and collides with the tag —
+                // hide the SensorStatusTagView to avoid collision
+                if let tag = tagLabel, !trendIsDownward {
                     SensorStatusTagView(text: tag.text, theme: tag.theme)
                     SensorStatusTagView(text: tag.text, theme: tag.theme)
+                        .offset(y: 14)
                         .zIndex(1)
                         .zIndex(1)
                 }
                 }
             }
             }
+            .onChange(of: glucose.last?.directionEnum) {
+                withAnimation {
+                    switch glucose.last?.directionEnum {
+                    case .doubleUp,
+                         .singleUp,
+                         .tripleUp:
+                        rotationDegrees = -90
+                    case .fortyFiveUp:
+                        rotationDegrees = -45
+                    case .flat:
+                        rotationDegrees = 0
+                    case .fortyFiveDown:
+                        rotationDegrees = 45
+                    case .doubleDown,
+                         .singleDown,
+                         .tripleDown:
+                        rotationDegrees = 90
+                    case nil,
+                         .notComputable,
+                         .rateOutOfRange:
+                        rotationDegrees = 0
+                    default:
+                        rotationDegrees = 0
+                    }
+                }
+            }
         } else {
         } else {
             VStack(alignment: .center, spacing: 12) {
             VStack(alignment: .center, spacing: 12) {
                 HStack
                 HStack
@@ -166,6 +171,11 @@ struct CurrentGlucoseView: View {
         return Date().timeIntervalSince(date) < 12 * 60
         return Date().timeIntervalSince(date) < 12 * 60
     }
     }
 
 
+    /// True when the trend arrow is rotated into the lower half of the
+    /// circle — used to decide whether the bottom tag needs to dodge the
+    /// triangle by sliding up onto the bobble's rim.
+    private var trendIsDownward: Bool { rotationDegrees >= 45 }
+
     /// Status highlight wins; otherwise fall back to remaining-time.
     /// Status highlight wins; otherwise fall back to remaining-time.
     private var tagLabel: (text: String, theme: SensorStatusTagTheme)? {
     private var tagLabel: (text: String, theme: SensorStatusTagTheme)? {
         if let status = cgmStatus {
         if let status = cgmStatus {

+ 1 - 2
Trio/Sources/Modules/Home/View/Header/SensorLifecycle/SensorStatusTagView.swift

@@ -14,8 +14,7 @@ struct SensorStatusTagView: View {
                 .foregroundStyle(theme.textColor)
                 .foregroundStyle(theme.textColor)
                 .lineLimit(1)
                 .lineLimit(1)
         }
         }
-        .padding(.vertical, 5)
-        .padding(.horizontal, 10)
+        .padding(.bottom, -16)
     }
     }
 }
 }