ソースを参照

Merge branch 'settings-update' into settings-update

Deniz Cengiz 1 年間 前
コミット
142a31d0c9
34 ファイル変更1601 行追加708 行削除
  1. 17 1
      FreeAPS.xcodeproj/project.pbxproj
  2. 3 4
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  3. 8 2
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  4. 16 8
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  5. 3 3
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  6. 8 8
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  7. 2 2
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  8. 6 6
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  9. 16 12
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  10. 7 7
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  11. 10 7
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  12. 9 32
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  13. 0 3
      FreeAPS/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift
  14. 12 0
      FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift
  15. 388 0
      FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  16. 3 0
      FreeAPS/Sources/Router/Screen.swift
  17. 23 16
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  18. 1 0
      FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift
  19. 4 0
      FreeAPS/Sources/Services/LiveActivity/Data/OverrideData.swift
  20. 18 2
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  21. 28 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  22. 78 30
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  23. 1 1
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  24. 193 0
      LiveActivity/LiveActivity+Helper.swift
  25. 183 562
      LiveActivity/LiveActivity.swift
  26. 20 0
      LiveActivity/Views/LiveActivityBGAndTrendView.swift
  27. 152 0
      LiveActivity/Views/LiveActivityChartView.swift
  28. 25 0
      LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift
  29. 222 0
      LiveActivity/Views/LiveActivityView.swift
  30. 22 0
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift
  31. 33 0
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  32. 42 0
      LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift
  33. 47 0
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  34. 1 0
      Model/Helper/CustomNotification.swift

+ 17 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 54;
+	objectVersion = 70;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -321,6 +321,7 @@
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
+		BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
@@ -451,6 +452,7 @@
 		DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD88C8E12C50420800F2D558 /* DefinitionRow.swift */; };
 		DD940BAA2CA7585D000830A5 /* GlucoseColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */; };
 		DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */; };
+		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
 		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
 		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
 		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
@@ -984,6 +986,7 @@
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
+		BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetConfiguration.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
@@ -1116,6 +1119,7 @@
 		DD88C8E12C50420800F2D558 /* DefinitionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefinitionRow.swift; sourceTree = "<group>"; };
 		DD940BA92CA7585D000830A5 /* GlucoseColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseColorScheme.swift; sourceTree = "<group>"; };
 		DD940BAB2CA75889000830A5 /* DynamicGlucoseColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicGlucoseColor.swift; sourceTree = "<group>"; };
+		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
 		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
 		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
 		DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
@@ -1187,6 +1191,10 @@
 		FEFFA7A12929FE49007B8193 /* UIDevice+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extensions.swift"; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+		DDCEBF412CC1B42500DF4C36 /* Views */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Views; sourceTree = "<group>"; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
 /* Begin PBXFrameworksBuildPhase section */
 		388E595525AD948C0019842D /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
@@ -2322,10 +2330,12 @@
 		6B1A8D1C2B14D91600E76752 /* LiveActivity */ = {
 			isa = PBXGroup;
 			children = (
+				DDCEBF412CC1B42500DF4C36 /* Views */,
 				6B1A8D1D2B14D91600E76752 /* LiveActivityBundle.swift */,
 				6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */,
 				6B1A8D232B14D91700E76752 /* Assets.xcassets */,
 				6B1A8D252B14D91700E76752 /* Info.plist */,
+				DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */,
 			);
 			path = LiveActivity;
 			sourceTree = "<group>";
@@ -2770,6 +2780,7 @@
 			isa = PBXGroup;
 			children = (
 				DDF847E32C5C288F0049BB3B /* LiveActivitySettingsRootView.swift */,
+				BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2967,6 +2978,9 @@
 			);
 			dependencies = (
 			);
+			fileSystemSynchronizedGroups = (
+				DDCEBF412CC1B42500DF4C36 /* Views */,
+			);
 			name = LiveActivityExtension;
 			productName = LiveActivityExtension;
 			productReference = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */;
@@ -3493,6 +3507,7 @@
 				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */,
 				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
@@ -3676,6 +3691,7 @@
 			files = (
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
 				6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */,
+				DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */,
 				6B1A8D1E2B14D91600E76752 /* LiveActivityBundle.swift in Sources */,
 				6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */,
 			);

+ 3 - 4
FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift

@@ -7,15 +7,14 @@ public func getDynamicGlucoseColor(
     highGlucoseColorValue: Decimal,
     lowGlucoseColorValue: Decimal,
     targetGlucose: Decimal,
-    glucoseColorScheme: GlucoseColorScheme,
-    offset: Decimal
+    glucoseColorScheme: GlucoseColorScheme
 ) -> Color {
     // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
     if glucoseColorScheme == .dynamicColor {
         return calculateHueBasedGlucoseColor(
             glucoseValue: glucoseValue,
-            highGlucose: highGlucoseColorValue + (offset * 1.75),
-            lowGlucose: lowGlucoseColorValue - offset,
+            highGlucose: highGlucoseColorValue,
+            lowGlucose: lowGlucoseColorValue,
             targetGlucose: targetGlucose
         )
     }

+ 8 - 2
FreeAPS/Sources/Models/DecimalPickerSettings.swift

@@ -18,10 +18,16 @@ class PickerSettingsProvider: ObservableObject {
         }
 
         // Glucose values are stored as mg/dl values, so Integers.
-        // Filter out odd numbers to avoid duplicate mmol/L values due to rounding.
+        // Filter out duplicate values when rounded to 1 decimal place.
         if units == .mmolL, setting.type == PickerSetting.PickerSettingType.glucose {
-            values = values.filter { Int($0) % 2 == 0 }
+            // Use a Set to track unique values rounded to 1 decimal
+            var uniqueRoundedValues = Set<String>()
+            values = values.filter { value in
+                let roundedValue = String(format: "%.1f", NSDecimalNumber(decimal: value.asMmolL).doubleValue)
+                return uniqueRoundedValues.insert(roundedValue).inserted
+            }
         }
+
         return values
     }
 }

+ 16 - 8
FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -48,10 +48,13 @@ extension AutosensSettings {
                     units: state.units,
                     type: .decimal("autosensMax"),
                     label: NSLocalizedString("Autosens Max", comment: "Autosens Max"),
-                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    miniHint:  """
+                    The higher limit of the Autosens Ratio
+                    Default: **120%**
+                    """,
                     verboseHint: Text(
                         NSLocalizedString(
-                            "This is a multiplier cap for autosens (and autotune) to set a 20% max limit on how high the autosens ratio can be, which in turn determines how high autosens can adjust basals, how low it can adjust ISF, and how low it can set the BG target.",
+                            "Autosens Max sets the maximum Autosens Ratio used by Autosens, Dynamic ISF, Sigmoid Formula, and/or Autotune. The Autosens Ratio is used to calculate the amount of adjustment needed to basals, ISF, and CR. Increasing this value allows automatic adjustments of basal rates to be higher, ISF to be lower, and CR to be lower. This can result in more insulin given.",
                             comment: "Autosens Max"
                         )
                     ),
@@ -72,12 +75,17 @@ extension AutosensSettings {
                     units: state.units,
                     type: .decimal("autosensMin"),
                     label: NSLocalizedString("Autosens Min", comment: "Autosens Min"),
-                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
-                    verboseHint: Text(
-                        NSLocalizedString(
-                            "The other side of the autosens safety limits, putting a cap on how low autosens can adjust basals, and how high it can adjust ISF and BG targets.",
-                            comment: "Autosens Min"
-                        )
+                    miniHint: """
+                    The lower limit of the Autosens Ratio
+                    Default: **80%**
+                    """,
+                    verboseHint: Text(NSLocalizedString(
+                        """
+                        Autosens Min sets the minimum Autosens Ratio used by Autosens, Dynamic ISF, Sigmoid Formula, and/or Autotune. 
+                        The Autosens Ratio is used to calculate the amount of adjustment needed to basals, ISF, and CR.
+                        Decreasing this value allows automatic adjustments of basal rates to be lower, ISF to be higher, and CR to be higher.
+                        """,
+                        comment: "Autosens Min")
                     )
                 )
 

+ 3 - 3
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -227,8 +227,8 @@ extension Bolus {
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
             forecastDisplayType = settings.settings.forecastDisplayType
-            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+            lowGlucose = settingsManager.settings.low
+            highGlucose = settingsManager.settings.high
             maxCarbs = settings.settings.maxCarbs
             maxFat = settings.settings.maxFat
             maxProtein = settings.settings.maxProtein
@@ -241,7 +241,7 @@ extension Bolus {
             let now = Date()
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm:ss"
+            dateFormatter.dateFormat = "HH:mm"
             dateFormatter.timeZone = TimeZone.current
 
             let entries: [(start: String, value: Decimal)]

+ 8 - 8
FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift

@@ -125,19 +125,19 @@ struct ForecastChart: View {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
             let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-
-            // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
-            let lowGlucose = state.units == .mgdL ? state.lowGlucose : state.lowGlucose.asMgdL
-            let highGlucose = state.units == .mgdL ? state.highGlucose : state.highGlucose.asMgdL
             let targetGlucose = (state.determination.first?.currentTarget ?? state.currentBGTarget as NSDecimalNumber) as Decimal
 
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = state.glucoseColorScheme == .dynamicColor
+
             let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
                 glucoseValue: Decimal(item.glucose),
-                highGlucoseColorValue: highGlucose,
-                lowGlucoseColorValue: lowGlucose,
+                highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : state.highGlucose,
+                lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : state.lowGlucose,
                 targetGlucose: targetGlucose,
-                glucoseColorScheme: state.glucoseColorScheme,
-                offset: 20
+                glucoseColorScheme: state.glucoseColorScheme
             )
 
             if !state.isSmoothingEnabled {

+ 2 - 2
FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift

@@ -22,7 +22,7 @@ extension GlucoseNotificationSettings {
                 addSourceInfoToGlucoseNotifications = $0 }
             subscribeSetting(\.lowGlucose, on: $lowGlucose, initial: {
                 let value = max(min($0, 400), 40)
-                lowGlucose = units == .mmolL ? value.asMmolL : value
+                lowGlucose = value
             }, map: {
                 guard units == .mmolL else { return $0 }
                 return $0.asMgdL
@@ -30,7 +30,7 @@ extension GlucoseNotificationSettings {
 
             subscribeSetting(\.highGlucose, on: $highGlucose, initial: {
                 let value = max(min($0, 400), 40)
-                highGlucose = units == .mmolL ? value.asMmolL : value
+                highGlucose = value
             }, map: {
                 guard units == .mmolL else { return $0 }
                 return $0.asMgdL

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

@@ -331,8 +331,8 @@ extension Home {
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             glucoseColorScheme = settingsManager.settings.glucoseColorScheme
             maxValue = settingsManager.preferences.autosensMax
-            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+            lowGlucose = settingsManager.settings.low
+            highGlucose = settingsManager.settings.high
             overrideUnit = settingsManager.settings.overrideHbA1cUnit
             displayXgridLines = settingsManager.settings.xGridLines
             displayYgridLines = settingsManager.settings.yGridLines
@@ -499,7 +499,7 @@ extension Home {
             let now = Date()
             let calendar = Calendar.current
             let dateFormatter = DateFormatter()
-            dateFormatter.dateFormat = "HH:mm:ss"
+            dateFormatter.dateFormat = "HH:mm"
             dateFormatter.timeZone = TimeZone.current
 
             let bgTargets = await provider.getBGTarget()
@@ -536,7 +536,7 @@ extension Home {
 
                 if now >= entryStartTime, now < entryEndTime {
                     await MainActor.run {
-                        currentGlucoseTarget = units == .mgdL ? entry.value : entry.value.asMmolL
+                        currentGlucoseTarget = entry.value
                     }
                     return
                 }
@@ -584,8 +584,8 @@ extension Home.StateModel:
         units = settingsManager.settings.units
         manualTempBasal = apsManager.isManualTempBasal
         isSmoothingEnabled = settingsManager.settings.smoothGlucose
-        lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-        highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+        lowGlucose = settingsManager.settings.low
+        highGlucose = settingsManager.settings.high
         Task {
             await getCurrentGlucoseTarget()
         }

+ 16 - 12
FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift

@@ -7,28 +7,32 @@ extension MainChartView {
     var staticYAxisChart: some View {
         Chart {
             /// high and low threshold lines
+
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
             if thresholdLines {
                 let highColor = FreeAPS.getDynamicGlucoseColor(
                     glucoseValue: highGlucose,
-                    highGlucoseColorValue: highGlucose,
-                    lowGlucoseColorValue: highGlucose,
-                    targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
-                    glucoseColorScheme: glucoseColorScheme,
-                    offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme
                 )
                 let lowColor = FreeAPS.getDynamicGlucoseColor(
                     glucoseValue: lowGlucose,
-                    highGlucoseColorValue: highGlucose,
-                    lowGlucoseColorValue: lowGlucose,
-                    targetGlucose: units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMmolL,
-                    glucoseColorScheme: glucoseColorScheme,
-                    offset: units == .mgdL ? Decimal(20) : Decimal(20).asMmolL
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                    targetGlucose: currentGlucoseTarget,
+                    glucoseColorScheme: glucoseColorScheme
                 )
 
-                RuleMark(y: .value("High", highGlucose))
+                RuleMark(y: .value("High", units == .mgdL ? highGlucose : highGlucose.asMmolL))
                     .foregroundStyle(highColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
-                RuleMark(y: .value("Low", lowGlucose))
+                RuleMark(y: .value("Low", units == .mgdL ? lowGlucose : lowGlucose.asMmolL))
                     .foregroundStyle(lowColor)
                     .lineStyle(.init(lineWidth: 1, dash: [5]))
             }

+ 7 - 7
FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift

@@ -19,17 +19,17 @@ struct GlucoseChartView: ChartContent {
         ForEach(glucoseData) { item in
             let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
 
-            // low glucose and high glucose is parsed in state to mmol/L; parse it back to mg/dL here for comparison
-            let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
-            let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
 
             let pointMarkColor: Color = FreeAPS.getDynamicGlucoseColor(
                 glucoseValue: Decimal(item.glucose),
-                highGlucoseColorValue: highGlucose,
-                lowGlucoseColorValue: lowGlucose,
+                highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
                 targetGlucose: currentGlucoseTarget,
-                glucoseColorScheme: glucoseColorScheme,
-                offset: 20
+                glucoseColorScheme: glucoseColorScheme
             )
 
             if !isSmoothingEnabled {

+ 10 - 7
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -243,7 +243,6 @@ extension MainChartView {
 
     @ViewBuilder var selectionPopover: some View {
         if let sgv = selectedGlucose?.glucose {
-            let glucoseToShow = units == .mgdL ? Decimal(sgv) : Decimal(sgv).asMmolL
             VStack(alignment: .leading) {
                 HStack {
                     Image(systemName: "clock")
@@ -251,16 +250,20 @@ extension MainChartView {
                         .font(.body).bold()
                 }.font(.body).padding(.bottom, 5)
 
+                // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                let hardCodedLow = Decimal(55)
+                let hardCodedHigh = Decimal(220)
+                let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
                 let glucoseColor = FreeAPS.getDynamicGlucoseColor(
-                    glucoseValue: glucoseToShow,
-                    highGlucoseColorValue: highGlucose,
-                    lowGlucoseColorValue: lowGlucose,
+                    glucoseValue: Decimal(sgv),
+                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
                     targetGlucose: currentGlucoseTarget,
-                    glucoseColorScheme: glucoseColorScheme,
-                    offset: units == .mgdL ? 20 : 20.asMmolL
+                    glucoseColorScheme: glucoseColorScheme
                 )
                 HStack {
-                    Text(units == .mgdL ? glucoseToShow.description : Decimal(sgv).formattedAsMmolL)
+                    Text(units == .mgdL ? Decimal(sgv).description : Decimal(sgv).formattedAsMmolL)
                         .bold()
                         + Text(" \(units.rawValue)")
                 }.foregroundStyle(

+ 9 - 32
FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift

@@ -81,21 +81,20 @@ struct CurrentGlucoseView: View {
                             let displayGlucose = units == .mgdL ? Decimal(glucoseValue).description : Decimal(glucoseValue)
                                 .formattedAsMmolL
 
-                            // low glucose, high glucose and target is parsed in state to mmol/L; parse it back to mg/dl here for comparison
-                            let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
-                            let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
-                            let targetGlucose = units == .mgdL ? currentGlucoseTarget : currentGlucoseTarget.asMgdL
-
                             var glucoseDisplayColor = Color.primary
 
+                            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                            let hardCodedLow = Decimal(55)
+                            let hardCodedHigh = Decimal(220)
+                            let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
                             if Decimal(glucoseValue) <= lowGlucose || Decimal(glucoseValue) >= highGlucose {
                                 glucoseDisplayColor = FreeAPS.getDynamicGlucoseColor(
                                     glucoseValue: Decimal(glucoseValue),
-                                    highGlucoseColorValue: highGlucose,
-                                    lowGlucoseColorValue: lowGlucose,
-                                    targetGlucose: targetGlucose,
-                                    glucoseColorScheme: glucoseColorScheme,
-                                    offset: 20
+                                    highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+                                    lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+                                    targetGlucose: currentGlucoseTarget,
+                                    glucoseColorScheme: glucoseColorScheme
                                 )
                             }
 
@@ -179,28 +178,6 @@ struct CurrentGlucoseView: View {
         let deltaAsDecimal = units == .mmolL ? Decimal(delta).asMmolL : Decimal(delta)
         return deltaFormatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
-
-//    var glucoseDisplayColor: Color {
-//        guard let lastGlucose = glucose.last?.glucose else { return .primary }
-//
-//        // low and high glucose is parsed in state to mmol/L; parse it back to mg/dl here for comparison
-//        let lowGlucose = units == .mgdL ? lowGlucose : lowGlucose.asMgdL
-//        let highGlucose = units == .mgdL ? highGlucose : highGlucose.asMgdL
-//
-//        // Ensure the thresholds are logical
-//        guard lowGlucose < highGlucose else { return .primary }
-//
-//        guard Decimal(lastGlucose) <= lowGlucose && Decimal(lastGlucose) >= highGlucose else { return .primary }
-//
-//        return FreeAPS.getDynamicGlucoseColor(
-//            glucoseValue: Decimal(lastGlucose),
-//            highGlucoseColorValue: highGlucose,
-//            lowGlucoseColorValue: lowGlucose,
-//            targetGlucose: currentGlucoseTarget,
-//            glucoseColorScheme: glucoseColorScheme,
-//            offset: 20
-//        )
-//    }
 }
 
 struct Triangle: Shape {

+ 0 - 3
FreeAPS/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift

@@ -3,16 +3,13 @@ import SwiftUI
 
 extension LiveActivitySettings {
     final class StateModel: BaseStateModel<Provider> {
-        @Injected() var settings: SettingsManager!
         @Injected() var storage: FileStorage!
 
         @Published var units: GlucoseUnits = .mgdL
         @Published var useLiveActivity = false
         @Published var lockScreenView: LockScreenView = .simple
-
         override func subscribe() {
             units = settingsManager.settings.units
-
             subscribeSetting(\.useLiveActivity, on: $useLiveActivity) { useLiveActivity = $0 }
             subscribeSetting(\.lockScreenView, on: $lockScreenView) { lockScreenView = $0 }
         }

+ 12 - 0
FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift

@@ -121,6 +121,18 @@ extension LiveActivitySettings {
                                     ).buttonStyle(BorderlessButtonStyle())
                                 }.padding(.top)
                             }.padding(.bottom)
+
+                            if state.lockScreenView == .detailed {
+                                HStack {
+                                    NavigationLink(
+                                        "Widget Configuration",
+                                        destination: LiveActivityWidgetConfiguration(
+                                            resolver: resolver,
+                                            state: state
+                                        )
+                                    ).foregroundStyle(Color.accentColor)
+                                }
+                            }
                         }.listRowBackground(Color.chart)
                     }
                 }

+ 388 - 0
FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift

@@ -0,0 +1,388 @@
+import Charts
+import Foundation
+import SwiftUI
+import Swinject
+import UniformTypeIdentifiers
+
+struct LiveActivityWidgetConfiguration: BaseView {
+    let resolver: Resolver
+
+    @ObservedObject var state: LiveActivitySettings.StateModel
+
+    @State private var selectedItems: [LiveActivityItem?] = Array(repeating: nil, count: 4)
+    @State private var showAddItemDialog: Bool = false
+    @State private var buttonIndexToUpdate: Int?
+    @State private var itemToRemove: LiveActivityItem?
+    @State private var isRemovalConfirmationPresented: Bool = false
+    @State private var glucoseData: [DummyGlucoseData] = []
+
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
+
+    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
+            )
+    }
+
+    private func generateDummyGlucoseData() -> [DummyGlucoseData] {
+        var data = [DummyGlucoseData]()
+        let totalMinutes = 6 * 60
+        let interval = 5
+
+        var glucoseLevel: Double = 90 // Start at a normal fasting glucose level
+
+        for minute in stride(from: 0, to: totalMinutes, by: interval) {
+            let time = Double(minute) / 60.0 // Convert minutes to hours
+
+            let trendFactor: Double
+            let randomFactor = Double.random(in: -5 ... 5) // Add slight randomness to each point
+
+            // Simulate different phases during the 6-hour window
+            if time < 1 { // Stable glucose (pre-meal or fasting period)
+                trendFactor = 0.5 + randomFactor // Small increase with some variability
+            } else if time >= 1, time < 2 { // Glucose rising (e.g., post-meal spike)
+                trendFactor = 3.0 + randomFactor // Rapid increase with slight variation
+            } else if time >= 2, time < 3.5 { // Peak and plateau
+                trendFactor = -0.1 + randomFactor // Gradual decrease after the peak with variability
+            } else if time >= 3.5, time < 4.5 { // Second peak (optional, simulate another meal)
+                trendFactor = 2.5 + randomFactor // Another spike with some randomness
+            } else { // Post-meal decrease (insulin effect)
+                trendFactor = -1.5 + randomFactor // Glucose decreasing gradually with some variability
+            }
+
+            // Calculate the next glucose level with trend factors
+            glucoseLevel += trendFactor
+
+            // Ensure glucose level doesn't go out of realistic bounds:
+            glucoseLevel = max(70, min(glucoseLevel, 200))
+
+            data.append(DummyGlucoseData(time: Double(minute), glucoseLevel: Int(glucoseLevel.rounded())))
+        }
+        return data
+    }
+
+    var body: some View {
+        VStack {
+            Group {
+                VStack(alignment: .trailing, spacing: 0) {
+                    Text("Live Activity Personalization".uppercased())
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                        .foregroundColor(.secondary)
+                        .font(.footnote)
+                        .padding(.leading)
+                }
+            }.padding(.bottom, -15)
+
+            GroupBox {
+                VStack {
+                    dummyChart(glucoseData)
+
+                    HStack(spacing: 15) {
+                        ForEach(0 ..< 4, id: \.self) { index in
+                            widgetButton(for: index)
+                        }
+                    }
+                    .padding()
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 12)
+                            .stroke(style: StrokeStyle(lineWidth: 2, dash: [5]))
+                            .foregroundColor(.gray)
+                    )
+                    .cornerRadius(12)
+                }
+
+            }.padding(.vertical).groupBoxStyle(.dummyChart)
+
+            Group {
+                HStack {
+                    Image(systemName: "info.circle")
+                    Text(
+                        "To re-order widgets, remove them and re-add them in the desired order."
+                    )
+                }
+            }.frame(maxWidth: .infinity, alignment: .leading)
+                .foregroundColor(.secondary)
+                .font(.footnote)
+                .padding(.horizontal)
+
+            Spacer()
+        }
+        .padding()
+        .scrollContentBackground(.hidden).background(color)
+        .navigationTitle("Widget Configuration")
+        .navigationBarTitleDisplayMode(.automatic)
+        .onAppear {
+            if glucoseData.isEmpty {
+                glucoseData = generateDummyGlucoseData()
+            }
+            loadOrder() // Load the saved order when the view appears
+        }
+        .confirmationDialog("Add Widget", isPresented: $showAddItemDialog, titleVisibility: .visible) {
+            ForEach(LiveActivityItem.allCases.filter { !selectedItems.contains($0) }, id: \.self) { item in
+                Button(item.displayName) {
+                    if let index = buttonIndexToUpdate {
+                        addItem(item, at: index)
+                    }
+                }
+            }
+        }
+    }
+
+    @ViewBuilder private func widgetButton(for index: Int) -> some View {
+        if index < selectedItems.count, let selectedItem = selectedItems[index] {
+            // Display selected item preview
+            ZStack(alignment: .topTrailing) {
+                getItemPreview(for: selectedItem)
+                    .frame(width: 50, height: 50)
+                    .padding(5)
+                    .background(Color.clear)
+                    .cornerRadius(12)
+                    .overlay(
+                        RoundedRectangle(cornerRadius: 12)
+                            .stroke(Color.primary, lineWidth: 1)
+                    )
+                Button(action: {
+                    isRemovalConfirmationPresented = true
+                    itemToRemove = selectedItem
+                }) {
+                    Image(systemName: "trash.circle.fill")
+                        .foregroundColor(Color(UIColor.systemGray2))
+                        .background(Color.white)
+                        .clipShape(Circle())
+                        .font(.system(size: 20))
+                }
+                .offset(x: 10, y: -10)
+                .confirmationDialog("Remove Widget", isPresented: $isRemovalConfirmationPresented, titleVisibility: .hidden) {
+                    Button("Remove Widget", role: .destructive) {
+                        if let itemToRemove = itemToRemove {
+                            removeItem(itemToRemove)
+                        }
+                    }
+                }
+            }
+        } else {
+            // Show "+" symbol for empty slots
+            Button(action: {
+                buttonIndexToUpdate = index
+                showAddItemDialog.toggle()
+            }) {
+                VStack {
+                    Image(systemName: "plus")
+                        .font(.title2)
+                        .foregroundColor(.accentColor)
+                }
+                .frame(width: 50, height: 50)
+                .padding(5)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 12)
+                        .stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
+                        .foregroundColor(.primary)
+                )
+            }
+            .buttonStyle(.plain)
+        }
+    }
+
+    private func getItemPreview(for item: LiveActivityItem) -> some View {
+        switch item {
+        case .currentGlucose:
+            return AnyView(currentGlucosePreview)
+        case .cob:
+            return AnyView(cobPreview)
+        case .iob:
+            return AnyView(iobPreview)
+        case .updatedLabel:
+            return AnyView(updatedLabelPreview)
+        }
+    }
+
+    @ViewBuilder private func dummyChart(_ glucoseData: [DummyGlucoseData]) -> some View {
+        Chart {
+            ForEach(glucoseData) { data in
+                let pointMarkColor = FreeAPS.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(data.glucoseLevel),
+                    highGlucoseColorValue: !(state.settingsManager.settings.glucoseColorScheme == .dynamicColor) ? state
+                        .settingsManager.settings.highGlucose : Decimal(220),
+                    lowGlucoseColorValue: !(state.settingsManager.settings.glucoseColorScheme == .dynamicColor) ? state
+                        .settingsManager.settings.lowGlucose : Decimal(55),
+                    targetGlucose: Decimal(100),
+                    glucoseColorScheme: state.settingsManager.settings.glucoseColorScheme
+                )
+
+                PointMark(
+                    x: .value("Time", data.time),
+                    y: .value("Glucose Level", data.glucoseLevel)
+                ).foregroundStyle(pointMarkColor).symbolSize(15)
+            }
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { _ in
+                AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
+                AxisValueLabel().foregroundStyle(.primary).font(.footnote)
+            }
+        }
+        .chartYScale(domain: 39 ... 200)
+        .chartYAxis(.hidden)
+        .chartPlotStyle { plotContent in
+            plotContent
+                .background(
+                    RoundedRectangle(cornerRadius: 12)
+                        .fill(Color.cyan.opacity(0.15))
+                )
+                .clipShape(RoundedRectangle(cornerRadius: 12))
+        }
+        .chartXAxis {
+            AxisMarks(position: .automatic) { _ in
+                AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.primary)
+            }
+        }
+        .frame(height: 100)
+    }
+
+    private var currentGlucosePreview: some View {
+        VStack {
+            HStack(alignment: .center) {
+                Text("123")
+                    .fontWeight(.bold)
+                    .font(.caption)
+            }
+            HStack(spacing: -5) {
+                HStack {
+                    Text("\u{2192}")
+                    Text("+6")
+                }.foregroundStyle(.primary).font(.caption2)
+            }
+        }
+    }
+
+    private var cobPreview: some View {
+        VStack(spacing: 2) {
+            Text("25 g").fontWeight(.bold).font(.caption)
+            Text("COB").font(.caption2).foregroundStyle(.primary)
+        }
+    }
+
+    private var iobPreview: some View {
+        VStack(spacing: 2) {
+            Text("2 U").fontWeight(.bold).font(.caption)
+            Text("IOB").font(.caption2).foregroundStyle(.primary)
+        }
+    }
+
+    private var updatedLabelPreview: some View {
+        VStack {
+            Text("19:05")
+                .fontWeight(.bold)
+                .font(.caption)
+                .foregroundStyle(.primary)
+
+            Text("Updated").font(.caption2).foregroundStyle(.primary)
+        }
+    }
+
+    private func loadOrder() {
+        if let savedItems = UserDefaults.standard.loadLiveActivityOrder() {
+            selectedItems = savedItems.count == 4 ? savedItems : savedItems + Array(repeating: nil, count: 4 - savedItems.count)
+        } else {
+            selectedItems = LiveActivityItem.defaultItems
+            saveOrder()
+        }
+    }
+
+    private func saveOrder() {
+        UserDefaults.standard.saveLiveActivityOrder(selectedItems)
+        Foundation.NotificationCenter.default.post(name: .liveActivityOrderDidChange, object: nil)
+    }
+
+    private func addItem(_ item: LiveActivityItem, at index: Int) {
+        selectedItems[index] = item
+        saveOrder()
+    }
+
+    private func removeItem(_ item: LiveActivityItem) {
+        if let index = selectedItems.firstIndex(of: item) {
+            selectedItems[index] = nil
+            saveOrder()
+        }
+    }
+}
+
+// Extension for UserDefaults to save and load the order
+extension UserDefaults {
+    private enum Keys {
+        static let liveActivityOrder = "liveActivityOrder"
+    }
+
+    func saveLiveActivityOrder(_ items: [LiveActivityItem?]) {
+        let itemStrings = items.map { $0?.rawValue ?? "" }
+        set(itemStrings, forKey: Keys.liveActivityOrder)
+    }
+
+    func loadLiveActivityOrder() -> [LiveActivityItem?]? {
+        if let itemStrings = array(forKey: Keys.liveActivityOrder) as? [String] {
+            return itemStrings.map { $0.isEmpty ? nil : LiveActivityItem(rawValue: $0) }
+        }
+        return nil
+    }
+}
+
+// Enum to represent each live activity item
+enum LiveActivityItem: String, CaseIterable, Identifiable {
+    case currentGlucose
+    case iob
+    case cob
+    case updatedLabel
+
+    var id: String { rawValue }
+
+    static var defaultItems: [LiveActivityItem] {
+        [.currentGlucose, .iob, .cob, .updatedLabel]
+    }
+
+    var displayName: String {
+        switch self {
+        case .currentGlucose:
+            return "Current Glucose"
+        case .iob:
+            return "IOB"
+        case .cob:
+            return "COB"
+        case .updatedLabel:
+            return "Last Updated"
+        }
+    }
+}
+
+struct DummyGlucoseData: Identifiable {
+    let id = UUID()
+    let time: Double // Time in hours
+    let glucoseLevel: Int // Glucose level in mg/dL
+}
+
+struct DummyChartGroupBoxStyle: GroupBoxStyle {
+    func makeBody(configuration: Configuration) -> some View {
+        VStack {
+            configuration.content
+        }
+        .padding()
+        .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
+        .background(Color.chart, in: RoundedRectangle(cornerRadius: 12))
+        .frame(width: UIScreen.main.bounds.width * 0.9)
+    }
+}
+
+extension GroupBoxStyle where Self == DummyChartGroupBoxStyle {
+    static var dummyChart: DummyChartGroupBoxStyle { .init() }
+}

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -40,6 +40,7 @@ enum Screen: Identifiable, Hashable {
     case featureSettings
     case notificationSettings
     case liveActivitySettings
+    case liveActivityBottomRowSettings
     case calendarEventSettings
     case serviceSettings
     case autosensSettings
@@ -130,6 +131,8 @@ extension Screen {
             NotificationsView(resolver: resolver, state: Settings.StateModel())
         case .liveActivitySettings:
             LiveActivitySettings.RootView(resolver: resolver)
+        case .liveActivityBottomRowSettings:
+            LiveActivityWidgetConfiguration(resolver: resolver, state: LiveActivitySettings.StateModel())
         case .calendarEventSettings:
             CalendarEventSettings.RootView(resolver: resolver)
         case .serviceSettings:

+ 23 - 16
FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -4,7 +4,7 @@ import Foundation
 
 @available(iOS 16.2, *)
 extension LiveActivityBridge {
-    func fetchAndMapGlucose() async {
+    func fetchAndMapGlucose() async -> [GlucoseData] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: context,
@@ -14,18 +14,18 @@ extension LiveActivityBridge {
             fetchLimit: 72
         )
 
-        await context.perform {
+        return await context.perform {
             guard let glucoseResults = results as? [GlucoseStored] else {
-                return
+                return []
             }
 
-            self.glucoseFromPersistence = glucoseResults.map {
+            return glucoseResults.map {
                 GlucoseData(glucose: Int($0.glucose), date: $0.date ?? Date(), direction: $0.directionEnum)
             }
         }
     }
 
-    func fetchAndMapDetermination() async {
+    func fetchAndMapDetermination() async -> DeterminationData? {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OrefDetermination.self,
             onContext: context,
@@ -33,24 +33,25 @@ extension LiveActivityBridge {
             key: "deliverAt",
             ascending: false,
             fetchLimit: 1,
-            propertiesToFetch: ["iob", "cob"]
+            propertiesToFetch: ["iob", "cob", "currentTarget"]
         )
 
-        await context.perform {
+        return await context.perform {
             guard let determinationResults = results as? [[String: Any]] else {
-                return
+                return nil
             }
 
-            self.determination = determinationResults.first.map {
+            return determinationResults.first.map {
                 DeterminationData(
                     cob: ($0["cob"] as? Int) ?? 0,
-                    iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0
+                    iob: ($0["iob"] as? NSDecimalNumber)?.decimalValue ?? 0,
+                    target: ($0["currentTarget"] as? NSDecimalNumber)?.decimalValue ?? 0
                 )
             }
         }
     }
 
-    func fetchAndMapOverride() async {
+    func fetchAndMapOverride() async -> OverrideData? {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: context,
@@ -58,16 +59,22 @@ extension LiveActivityBridge {
             key: "date",
             ascending: false,
             fetchLimit: 1,
-            propertiesToFetch: ["enabled"]
+            propertiesToFetch: ["enabled", "name", "target", "date", "duration"]
         )
 
-        await context.perform {
+        return await context.perform {
             guard let overrideResults = results as? [[String: Any]] else {
-                return
+                return nil
             }
 
-            self.isOverridesActive = overrideResults.first.map {
-                OverrideData(isActive: $0["enabled"] as? Bool ?? false)
+            return overrideResults.first.map {
+                OverrideData(
+                    isActive: $0["enabled"] as? Bool ?? false,
+                    overrideName: $0["name"] as? String ?? "Override",
+                    date: $0["date"] as? Date ?? Date(),
+                    duration: $0["duration"] as? Decimal ?? 0,
+                    target: $0["target"] as? Decimal ?? 0
+                )
             }
         }
     }

+ 1 - 0
FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift

@@ -3,4 +3,5 @@ import Foundation
 struct DeterminationData {
     let cob: Int
     let iob: Decimal
+    let target: Decimal
 }

+ 4 - 0
FreeAPS/Sources/Services/LiveActivity/Data/OverrideData.swift

@@ -2,4 +2,8 @@ import Foundation
 
 struct OverrideData {
     let isActive: Bool
+    let overrideName: String
+    let date: Date
+    let duration: Decimal
+    let target: Decimal
 }

+ 18 - 2
FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift

@@ -2,13 +2,24 @@ import ActivityKit
 import Foundation
 
 struct LiveActivityAttributes: ActivityAttributes {
-    public struct ContentState: Codable, Hashable {
+    enum LiveActivityItem: String, Hashable, Codable, Equatable {
+        case currentGlucose
+        case iob
+        case cob
+        case updatedLabel
+        case empty
+
+        static let defaultItems: [Self] = [.currentGlucose, .iob, .cob, .updatedLabel]
+    }
+
+    struct ContentState: Codable, Hashable {
         let bg: String
         let direction: String?
         let change: String
         let date: Date
         let highGlucose: Decimal
         let lowGlucose: Decimal
+        let target: Decimal
         let glucoseColorScheme: String
         let detailedViewState: ContentAdditionalState?
 
@@ -16,7 +27,7 @@ struct LiveActivityAttributes: ActivityAttributes {
         let isInitialState: Bool
     }
 
-    public struct ContentAdditionalState: Codable, Hashable {
+    struct ContentAdditionalState: Codable, Hashable {
         let chart: [Decimal]
         let chartDate: [Date?]
         let rotationDegrees: Double
@@ -24,6 +35,11 @@ struct LiveActivityAttributes: ActivityAttributes {
         let iob: Decimal
         let unit: String
         let isOverrideActive: Bool
+        let overrideName: String
+        let overrideDate: Date
+        let overrideDuration: Decimal
+        let overrideTarget: Decimal
+        let widgetItems: [LiveActivityItem]
     }
 
     let startDate: Date

+ 28 - 2
FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -1,5 +1,24 @@
 import Foundation
 
+extension UserDefaults {
+    private enum Keys {
+        static let liveActivityOrder = "liveActivityOrder"
+    }
+
+    func loadLiveActivityOrderFromUserDefaults() -> [LiveActivityAttributes.LiveActivityItem]? {
+        if let itemStrings = stringArray(forKey: Keys.liveActivityOrder) {
+            return itemStrings.map { string in
+                if string == "" {
+                    return .empty
+                } else {
+                    return LiveActivityAttributes.LiveActivityItem(rawValue: string) ?? .empty
+                }
+            }
+        }
+        return nil
+    }
+}
+
 extension LiveActivityAttributes.ContentState {
     static func formatGlucose(_ value: Int, units: GlucoseUnits, forceSign: Bool) -> String {
         let formatter = NumberFormatter()
@@ -43,7 +62,8 @@ extension LiveActivityAttributes.ContentState {
         chart: [GlucoseData],
         settings: FreeAPSSettings,
         determination: DeterminationData?,
-        override: OverrideData?
+        override: OverrideData?,
+        widgetItems: [LiveActivityAttributes.LiveActivityItem]?
     ) {
         let glucose = bg.glucose
         let formattedBG = Self.formatGlucose(Int(glucose), units: units, forceSign: false)
@@ -90,7 +110,12 @@ extension LiveActivityAttributes.ContentState {
                 cob: Decimal(determination?.cob ?? 0),
                 iob: determination?.iob ?? 0 as Decimal,
                 unit: settings.units.rawValue,
-                isOverrideActive: override?.isActive ?? false
+                isOverrideActive: override?.isActive ?? false,
+                overrideName: override?.overrideName ?? "Override",
+                overrideDate: override?.date ?? Date(),
+                overrideDuration: override?.duration ?? 0,
+                overrideTarget: override?.target ?? 0,
+                widgetItems: widgetItems ?? LiveActivityAttributes.LiveActivityItem.defaultItems
             )
 
         case .simple:
@@ -104,6 +129,7 @@ extension LiveActivityAttributes.ContentState {
             date: bg.date,
             highGlucose: settings.high,
             lowGlucose: settings.low,
+            target: determination?.target ?? 100 as Decimal,
             glucoseColorScheme: settings.glucoseColorScheme.rawValue,
             detailedViewState: detailedState,
             isInitialState: false

+ 78 - 30
FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -25,7 +25,7 @@ import UIKit
     }
 }
 
-@available(iOS 16.2, *) final class LiveActivityBridge: Injectable, ObservableObject
+@available(iOS 16.2, *) final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver
 {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
@@ -43,7 +43,8 @@ import UIKit
     private var currentActivity: ActiveActivity?
     private var latestGlucose: GlucoseData?
     var glucoseFromPersistence: [GlucoseData]?
-    var isOverridesActive: OverrideData?
+    var override: OverrideData?
+    var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
 
     let context = CoreDataStack.shared.newTaskContext()
 
@@ -64,6 +65,7 @@ import UIKit
         registerHandler()
         monitorForLiveActivityAuthorizationChanges()
         setupGlucoseArray()
+        broadcaster.register(SettingsObserver.self, observer: self)
     }
 
     private func setupNotifications() {
@@ -80,6 +82,20 @@ import UIKit
                     self?.forceActivityUpdate()
                 }
             }
+        notificationCenter.addObserver(
+            self,
+            selector: #selector(handleLiveActivityOrderChange),
+            name: .liveActivityOrderDidChange,
+            object: nil
+        )
+    }
+
+    // TODO: - use a delegate or a custom notification here instead
+
+    func settingsDidChange(_: FreeAPSSettings) {
+        Task {
+            await updateContentState(determination)
+        }
     }
 
     private func registerHandler() {
@@ -106,30 +122,78 @@ import UIKit
     }
 
     private func cobOrIobDidUpdate() {
-        Task {
-            await fetchAndMapDetermination()
+        Task { @MainActor in
+            self.determination = await fetchAndMapDetermination()
             if let determination = determination {
-                await self.pushDeterminationUpdate(determination)
+                await self.updateContentState(determination)
             }
         }
     }
 
     private func overridesDidUpdate() {
-        Task {
-            await fetchAndMapOverride()
+        Task { @MainActor in
+            self.override = await fetchAndMapOverride()
             if let determination = determination {
-                await self.pushDeterminationUpdate(determination)
+                await self.updateContentState(determination)
             }
         }
     }
 
-    private func setupGlucoseArray() {
+    @objc private func handleLiveActivityOrderChange() {
+        Task {
+            self.widgetItems = UserDefaults.standard
+                .loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes.LiveActivityItem.defaultItems
+            await self.updateLiveActivityOrder()
+        }
+    }
+
+    @MainActor private func updateContentState<T>(_ update: T) async {
+        guard let latestGlucose = latestGlucose else { return }
+
+        var content: LiveActivityAttributes.ContentState?
+
+        if let determination = update as? DeterminationData {
+            content = LiveActivityAttributes.ContentState(
+                new: latestGlucose,
+                prev: latestGlucose,
+                units: settings.units,
+                chart: glucoseFromPersistence ?? [],
+                settings: settings,
+                determination: determination,
+                override: override,
+                widgetItems: widgetItems
+            )
+        } else if let override = update as? OverrideData {
+            content = LiveActivityAttributes.ContentState(
+                new: latestGlucose,
+                prev: latestGlucose,
+                units: settings.units,
+                chart: glucoseFromPersistence ?? [],
+                settings: settings,
+                determination: determination,
+                override: override,
+                widgetItems: widgetItems
+            )
+        }
+
+        if let content = content {
+            await pushUpdate(content)
+        }
+    }
+
+    @MainActor private func updateLiveActivityOrder() async {
         Task {
+            await updateContentState(determination)
+        }
+    }
+
+    private func setupGlucoseArray() {
+        Task { @MainActor in
             // Fetch and map glucose to GlucoseData struct
-            await fetchAndMapGlucose()
+            self.glucoseFromPersistence = await fetchAndMapGlucose()
 
             // Push the update to the Live Activity
-            await glucoseDidUpdate(glucoseFromPersistence ?? [])
+            glucoseDidUpdate(glucoseFromPersistence ?? [])
         }
     }
 
@@ -195,6 +259,7 @@ import UIKit
                         date: Date.now,
                         highGlucose: settings.high,
                         lowGlucose: settings.low,
+                        target: determination?.target ?? 100 as Decimal,
                         glucoseColorScheme: settings.glucoseColorScheme.rawValue,
                         detailedViewState: nil,
                         isInitialState: true
@@ -218,24 +283,6 @@ import UIKit
         }
     }
 
-    @MainActor private func pushDeterminationUpdate(_ determination: DeterminationData) async {
-        guard let latestGlucose = latestGlucose else { return }
-
-        let content = LiveActivityAttributes.ContentState(
-            new: latestGlucose,
-            prev: latestGlucose,
-            units: settings.units,
-            chart: glucoseFromPersistence ?? [],
-            settings: settings,
-            determination: determination,
-            override: isOverridesActive
-        )
-
-        if let content = content {
-            await pushUpdate(content)
-        }
-    }
-
     /// ends all live activities immediateny
     private func endActivity() async {
         if let currentActivity {
@@ -282,7 +329,8 @@ extension LiveActivityBridge {
                 chart: glucose,
                 settings: settings,
                 determination: determination,
-                override: isOverridesActive
+                override: override,
+                widgetItems: widgetItems
             )
 
             if let content = content {

+ 1 - 1
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -531,7 +531,7 @@ extension BaseTidepoolManager {
         let now = Date()
         let calendar = Calendar.current
         let dateFormatter = DateFormatter()
-        dateFormatter.dateFormat = "HH:mm:ss"
+        dateFormatter.dateFormat = "HH:mm"
         dateFormatter.timeZone = TimeZone.current
 
         let basalEntries = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self)

+ 193 - 0
LiveActivity/LiveActivity+Helper.swift

@@ -0,0 +1,193 @@
+//
+//  LiveActivity+Helper.swift
+//  LiveActivityExtension
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import ActivityKit
+import Charts
+import SwiftUI
+import WidgetKit
+
+enum Size {
+    case minimal
+    case compact
+    case expanded
+}
+
+enum GlucoseUnits: String, Equatable {
+    case mgdL = "mg/dL"
+    case mmolL = "mmol/L"
+
+    static let exchangeRate: Decimal = 0.0555
+}
+
+enum GlucoseColorScheme: String, Equatable {
+    case staticColor
+    case dynamicColor
+}
+
+func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
+    var result = Decimal()
+    var toRound = value
+    NSDecimalRound(&result, &toRound, scale, roundingMode)
+    return result
+}
+
+extension Int {
+    var asMmolL: Decimal {
+        rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
+    }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
+    }
+}
+
+extension Decimal {
+    var asMmolL: Decimal {
+        rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
+    }
+
+    var asMgdL: Decimal {
+        rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
+    }
+
+    var formattedAsMmolL: String {
+        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
+    }
+}
+
+extension NumberFormatter {
+    static let glucoseFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.locale = Locale.current
+        formatter.numberStyle = .decimal
+        formatter.minimumFractionDigits = 1
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }()
+}
+
+extension Color {
+    // Helper function to decide how to pick the glucose color
+    static func getDynamicGlucoseColor(
+        glucoseValue: Decimal,
+        highGlucoseColorValue: Decimal,
+        lowGlucoseColorValue: Decimal,
+        targetGlucose: Decimal,
+        glucoseColorScheme: String
+    ) -> Color {
+        // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
+        if glucoseColorScheme == "dynamicColor" {
+            return calculateHueBasedGlucoseColor(
+                glucoseValue: glucoseValue,
+                highGlucose: highGlucoseColorValue,
+                lowGlucose: lowGlucoseColorValue,
+                targetGlucose: targetGlucose
+            )
+        }
+        // Otheriwse, use static (orange = high, red = low, green = range)
+        else {
+            if glucoseValue >= highGlucoseColorValue {
+                return Color.orange
+            } else if glucoseValue <= lowGlucoseColorValue {
+                return Color.red
+            } else {
+                return Color.green
+            }
+        }
+    }
+
+    // Dynamic color - Define the hue values for the key points
+    // We'll shift color gradually one glucose point at a time
+    // We'll shift through the rainbow colors of ROY-G-BIV from low to high
+    // Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
+    private static func calculateHueBasedGlucoseColor(
+        glucoseValue: Decimal,
+        highGlucose: Decimal,
+        lowGlucose: Decimal,
+        targetGlucose: Decimal
+    ) -> Color {
+        let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
+        let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
+        let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
+
+        // Calculate the hue based on the bgLevel
+        var hue: CGFloat
+        if glucoseValue <= lowGlucose {
+            hue = redHue
+        } else if glucoseValue >= highGlucose {
+            hue = purpleHue
+        } else if glucoseValue <= targetGlucose {
+            // Interpolate between red and green
+            let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
+
+            hue = redHue + ratio * (greenHue - redHue)
+        } else {
+            // Interpolate between green and purple
+            let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
+            hue = greenHue + ratio * (purpleHue - greenHue)
+        }
+        // Return the color with full saturation and brightness
+        let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
+        return color
+    }
+}
+
+func bgAndTrend(
+    context: ActivityViewContext<LiveActivityAttributes>,
+    size: Size,
+    glucoseColor: Color
+) -> (some View, Int) {
+    let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+
+    var characters = 0
+
+    let bgText = context.state.bg
+    characters += bgText.count
+
+    // narrow mode is for the minimal dynamic island view
+    // there is not enough space to show all three arrow there
+    // and everything has to be squeezed together to some degree
+    // only display the first arrow character and make it red in case there were more characters
+    var directionText: String?
+    if let direction = context.state.direction {
+        if size == .compact || size == .minimal {
+            directionText = String(direction[direction.startIndex ... direction.startIndex])
+        } else {
+            directionText = direction
+        }
+
+        characters += directionText!.count
+    }
+
+    let spacing: CGFloat
+    switch size {
+    case .minimal: spacing = -1
+    case .compact: spacing = 0
+    case .expanded: spacing = 3
+    }
+
+    let stack = HStack(spacing: spacing) {
+        Text(bgText)
+            .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
+            .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+        if let direction = directionText {
+            let text = Text(direction)
+            switch size {
+            case .minimal:
+                let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
+                scaledText.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
+            case .compact:
+                text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
+
+            case .expanded:
+                text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading).padding(.trailing, -5)
+            }
+        }
+    }.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
+        .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+    return (stack, characters)
+}

+ 183 - 562
LiveActivity/LiveActivity.swift

@@ -1,578 +1,69 @@
 import ActivityKit
-import Charts
-import Foundation
 import SwiftUI
 import WidgetKit
 
-private enum Size {
-    case minimal
-    case compact
-    case expanded
-}
-
-enum GlucoseUnits: String, Equatable {
-    case mgdL = "mg/dL"
-    case mmolL = "mmol/L"
-
-    static let exchangeRate: Decimal = 0.0555
-}
-
-enum GlucoseColorScheme: String, Equatable {
-    case staticColor
-    case dynamicColor
-}
-
-func rounded(_ value: Decimal, scale: Int, roundingMode: NSDecimalNumber.RoundingMode) -> Decimal {
-    var result = Decimal()
-    var toRound = value
-    NSDecimalRound(&result, &toRound, scale, roundingMode)
-    return result
-}
-
-extension Int {
-    var asMmolL: Decimal {
-        rounded(Decimal(self) * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
-    }
-
-    var formattedAsMmolL: String {
-        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
-    }
-}
-
-extension Decimal {
-    var asMmolL: Decimal {
-        rounded(self * GlucoseUnits.exchangeRate, scale: 1, roundingMode: .plain)
-    }
-
-    var asMgdL: Decimal {
-        rounded(self / GlucoseUnits.exchangeRate, scale: 0, roundingMode: .plain)
-    }
-
-    var formattedAsMmolL: String {
-        NumberFormatter.glucoseFormatter.string(from: asMmolL as NSDecimalNumber) ?? "\(asMmolL)"
-    }
-}
-
-extension NumberFormatter {
-    static let glucoseFormatter: NumberFormatter = {
-        let formatter = NumberFormatter()
-        formatter.locale = Locale.current
-        formatter.numberStyle = .decimal
-        formatter.minimumFractionDigits = 1
-        formatter.maximumFractionDigits = 1
-        return formatter
-    }()
-}
-
 struct LiveActivity: Widget {
-    // Helper function to decide how to pick the glucose color
-    func getDynamicGlucoseColor(
-        glucoseValue: Decimal,
-        highGlucoseColorValue: Decimal,
-        lowGlucoseColorValue: Decimal,
-        targetGlucose: Decimal,
-        glucoseColorScheme: String,
-        offset: Decimal
-    ) -> Color {
-        // Convert Decimal to Int for high and low glucose values
-        let lowGlucose = lowGlucoseColorValue - offset
-        let highGlucose = highGlucoseColorValue + (offset * 1.75)
-        let targetGlucose = targetGlucose
-
-        // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
-        if glucoseColorScheme == "dynamicColor" {
-            return calculateHueBasedGlucoseColor(
-                glucoseValue: glucoseValue,
-                highGlucose: highGlucose,
-                lowGlucose: lowGlucose,
-                targetGlucose: targetGlucose
-            )
-        }
-        // Otheriwse, use static (orange = high, red = low, green = range)
-        else {
-            if glucoseValue > highGlucose {
-                return Color.orange
-            } else if glucoseValue < lowGlucose {
-                return Color.red
-            } else {
-                return Color.green
-            }
-        }
-    }
-
-    // Dynamic color - Define the hue values for the key points
-    // We'll shift color gradually one glucose point at a time
-    // We'll shift through the rainbow colors of ROY-G-BIV from low to high
-    // Start at red for lowGlucose, green for targetGlucose, and violet for highGlucose
-    func calculateHueBasedGlucoseColor(
-        glucoseValue: Decimal,
-        highGlucose: Decimal,
-        lowGlucose: Decimal,
-        targetGlucose: Decimal
-    ) -> Color {
-        let redHue: CGFloat = 0.0 / 360.0 // 0 degrees
-        let greenHue: CGFloat = 120.0 / 360.0 // 120 degrees
-        let purpleHue: CGFloat = 270.0 / 360.0 // 270 degrees
-
-        // Calculate the hue based on the bgLevel
-        var hue: CGFloat
-        if glucoseValue <= lowGlucose {
-            hue = redHue
-        } else if glucoseValue >= highGlucose {
-            hue = purpleHue
-        } else if glucoseValue <= targetGlucose {
-            // Interpolate between red and green
-            let ratio = CGFloat(truncating: (glucoseValue - lowGlucose) / (targetGlucose - lowGlucose) as NSNumber)
-
-            hue = redHue + ratio * (greenHue - redHue)
-        } else {
-            // Interpolate between green and purple
-            let ratio = CGFloat(truncating: (glucoseValue - targetGlucose) / (highGlucose - targetGlucose) as NSNumber)
-            hue = greenHue + ratio * (purpleHue - greenHue)
-        }
-        // Return the color with full saturation and brightness
-        let color = Color(hue: hue, saturation: 0.6, brightness: 0.9)
-        return color
-    }
-
-    private let dateFormatter: DateFormatter = {
-        var f = DateFormatter()
-        f.dateStyle = .none
-        f.timeStyle = .short
-        return f
-    }()
-
-    private var bolusFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 2
-        formatter.decimalSeparator = "."
-        return formatter
-    }
-
-    private var carbsFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        return formatter
-    }
-
-    @ViewBuilder private func changeLabel(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        if !context.state.change.isEmpty {
-            Text(context.state.change).foregroundStyle(.primary).font(.headline)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-        } else {
-            Text("--")
-        }
-    }
-
-    @ViewBuilder func mealLabel(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        HStack {
-            VStack(alignment: .leading, spacing: 1, content: {
-                HStack {
-                    Image(systemName: "fork.knife")
-                        .font(.title3)
-                        .foregroundColor(.yellow)
-                }
-                HStack {
-                    Image(systemName: "syringe.fill")
-                        .font(.title3)
-                        .foregroundColor(.blue)
-                }
-            })
-            VStack(alignment: .trailing, spacing: 1, content: {
-                HStack {
-                    Text(
-                        carbsFormatter.string(from: additionalState.cob as NSNumber) ?? "--"
-                    ).fontWeight(.bold).font(.headline).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-                    Text(NSLocalizedString(" g", comment: "grams of carbs")).foregroundStyle(.secondary).font(.footnote)
-                }
-                HStack {
-                    Text(
-                        bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
-                    ).font(.headline).fontWeight(.bold).strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-                    Text(NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)"))
-                        .foregroundStyle(.secondary).font(.footnote)
-                }
-            })
-            VStack(alignment: .trailing, spacing: 1, content: {
-                if additionalState.isOverrideActive {
-                    Image(systemName: "person.crop.circle.fill.badge.checkmark")
-                        .font(.title3)
-                }
-            })
-        }
-    }
-
-    @ViewBuilder func trend(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        if context.isStale {
-            Text("--")
-        } else {
-            if let trendSystemImage = context.state.direction {
-                Image(systemName: trendSystemImage)
-            }
-        }
-    }
-
-    private func expiredLabel() -> some View {
-        Text("Live Activity Expired. Open Trio to Refresh")
-            .minimumScaleFactor(0.01)
-    }
-
-    private func updatedLabel(context: ActivityViewContext<LiveActivityAttributes>) -> Text {
-        let text = Text("Updated: \(dateFormatter.string(from: context.state.date))")
-            .font(.caption2)
-        if context.isStale {
-            // foregroundStyle is not available in <iOS 17 hence the check here
-            if #available(iOSApplicationExtension 17.0, *) {
-                return text.bold().foregroundStyle(.red)
-            } else {
-                return text.bold().foregroundColor(.red)
-            }
-        } else {
-            if #available(iOSApplicationExtension 17.0, *) {
-                return text.bold().foregroundStyle(.secondary)
-            } else {
-                return text.bold().foregroundColor(.red)
-            }
-        }
-    }
-
-    @ViewBuilder private func bgLabel(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        HStack(alignment: .center) {
-            Text(context.state.bg)
-                .fontWeight(.bold)
-                .font(.largeTitle)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-            Text(additionalState.unit).foregroundStyle(.secondary).font(.subheadline).offset(x: -5, y: 5)
-        }
-    }
-
-    private func bgAndTrend(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        size: Size,
-        hasStaticColorScheme: Bool,
-        glucoseColor: Color
-    ) -> (some View, Int) {
-        var characters = 0
-
-        let bgText = context.state.bg
-        characters += bgText.count
-
-        // narrow mode is for the minimal dynamic island view
-        // there is not enough space to show all three arrow there
-        // and everything has to be squeezed together to some degree
-        // only display the first arrow character
-        var directionText: String?
-        if let direction = context.state.direction {
-            if size == .compact {
-                directionText = String(direction[direction.startIndex ... direction.startIndex])
-            } else {
-                directionText = direction
+    var body: some WidgetConfiguration {
+        ActivityConfiguration(for: LiveActivityAttributes.self) { context in
+            LiveActivityView(context: context)
+        } dynamicIsland: { context in
+            let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+
+            var glucoseColor: Color {
+                let state = context.state
+                let detailedState = state.detailedViewState
+                let isMgdL = detailedState?.unit == "mg/dL"
+
+                // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+                let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
+                let hardCodedHigh = isMgdL ? Decimal(220) : 220.asMmolL
+
+                return Color.getDynamicGlucoseColor(
+                    glucoseValue: Decimal(string: state.bg) ?? 100,
+                    highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : state.highGlucose,
+                    lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : state.lowGlucose,
+                    targetGlucose: isMgdL ? state.target : state.target.asMmolL,
+                    glucoseColorScheme: state.glucoseColorScheme
+                )
             }
 
-            characters += directionText!.count
-        }
-
-        let spacing: CGFloat
-        switch size {
-        case .minimal: spacing = -1
-        case .compact: spacing = 0
-        case .expanded: spacing = 3
-        }
-
-        let stack = HStack(spacing: spacing) {
-            Text(bgText)
-                .foregroundColor(hasStaticColorScheme ? .primary : glucoseColor)
-                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
-
-            if let direction = directionText {
-                let text = Text(direction)
-                switch size {
-                case .minimal:
-                    let scaledText = text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading)
-                    scaledText.foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
-                case .compact:
-                    text.scaleEffect(x: 0.8, y: 0.8, anchor: .leading).padding(.trailing, -3)
-
-                case .expanded:
-                    text.scaleEffect(x: 0.7, y: 0.7, anchor: .leading).padding(.trailing, -5)
+            return DynamicIsland {
+                DynamicIslandExpandedRegion(.leading) {
+                    LiveActivityExpandedLeadingView(context: context, glucoseColor: glucoseColor)
                 }
-            }
-        }
-        .foregroundColor(context.isStale ? Color.primary.opacity(0.5) : (hasStaticColorScheme ? .primary : glucoseColor))
-
-        return (stack, characters)
-    }
-
-    @ViewBuilder func trendArrow(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        let gradient = LinearGradient(colors: [
-            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
-            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
-            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
-            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
-        ], startPoint: .leading, endPoint: .trailing)
-
-        if !context.isStale {
-            Image(systemName: "arrow.right")
-                .font(.title)
-                .rotationEffect(.degrees(additionalState.rotationDegrees))
-                .foregroundStyle(gradient)
-        }
-    }
-
-    @ViewBuilder func chart(
-        context: ActivityViewContext<LiveActivityAttributes>,
-        additionalState: LiveActivityAttributes.ContentAdditionalState
-    ) -> some View {
-        if context.isStale {
-            Text("No data available")
-        } else {
-            // Determine scale
-            let min = min(additionalState.chart.min() ?? 45, 40) - 20
-            let max = max(additionalState.chart.max() ?? 270, 300) + 50
-
-            let yAxisRuleMarkMin = additionalState.unit == "mg/dL" ? context.state.lowGlucose : context.state.lowGlucose
-                .asMmolL
-            let yAxisRuleMarkMax = additionalState.unit == "mg/dL" ? context.state.highGlucose : context.state.highGlucose
-                .asMmolL
-
-            // TODO: grab target from proper targets, do not hard code.
-            let highColor = getDynamicGlucoseColor(
-                glucoseValue: yAxisRuleMarkMax,
-                highGlucoseColorValue: yAxisRuleMarkMax,
-                lowGlucoseColorValue: yAxisRuleMarkMin,
-                targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
-                glucoseColorScheme: context.state.glucoseColorScheme,
-                offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
-            )
-
-            // TODO: grab target from proper targets, do not hard code.
-            let lowColor = getDynamicGlucoseColor(
-                glucoseValue: yAxisRuleMarkMin,
-                highGlucoseColorValue: yAxisRuleMarkMax,
-                lowGlucoseColorValue: yAxisRuleMarkMin,
-                targetGlucose: additionalState.unit == "mg/dL" ? Decimal(90) : Decimal(90).asMmolL,
-                glucoseColorScheme: context.state.glucoseColorScheme,
-                offset: additionalState.unit == "mg/dL" ? Decimal(20) : Decimal(20).asMmolL
-            )
-
-            Chart {
-                RuleMark(y: .value("High", yAxisRuleMarkMax))
-                    .foregroundStyle(highColor)
-                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
-                RuleMark(y: .value("Low", yAxisRuleMarkMin))
-                    .foregroundStyle(lowColor)
-                    .lineStyle(.init(lineWidth: 0.5, dash: [5]))
-
-                ForEach(additionalState.chart.indices, id: \.self) { index in
-                    let currentValue = additionalState.chart[index]
-                    let displayValue = additionalState.unit == "mg/dL" ? currentValue : currentValue.asMmolL
-
-                    // TODO: grab target from proper targets, do not hard code.
-                    let pointMarkColor = self.getDynamicGlucoseColor(
-                        glucoseValue: currentValue,
-                        highGlucoseColorValue: context.state.highGlucose,
-                        lowGlucoseColorValue: context.state.lowGlucose,
-                        targetGlucose: 90,
-                        glucoseColorScheme: context.state.glucoseColorScheme,
-                        offset: 20
+                DynamicIslandExpandedRegion(.trailing) {
+                    LiveActivityExpandedTrailingView(
+                        context: context,
+                        glucoseColor: hasStaticColorScheme ? .primary : glucoseColor
                     )
-
-                    let chartDate = additionalState.chartDate[index] ?? Date()
-
-                    let pointMark = PointMark(
-                        x: .value("Time", chartDate),
-                        y: .value("Value", displayValue)
-                    ).symbolSize(15)
-
-                    pointMark.foregroundStyle(pointMarkColor)
                 }
-            }
-            .chartYAxis {
-                AxisMarks(position: .trailing) { _ in
-                    AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
-                    AxisValueLabel().foregroundStyle(.secondary).font(.footnote)
+                DynamicIslandExpandedRegion(.bottom) {
+                    LiveActivityExpandedBottomView(context: context)
                 }
-            }
-            .chartYScale(domain: additionalState.unit == "mg/dL" ? min ... max : min.asMmolL ... max.asMmolL)
-            .chartXAxis {
-                AxisMarks(position: .automatic) { _ in
-                    AxisGridLine(stroke: .init(lineWidth: 0.2, dash: [2, 3])).foregroundStyle(Color.white)
+                DynamicIslandExpandedRegion(.center) {
+                    LiveActivityExpandedCenterView(context: context)
                 }
+            } compactLeading: {
+                LiveActivityCompactLeadingView(context: context, glucoseColor: glucoseColor)
+            } compactTrailing: {
+                LiveActivityCompactTrailingView(context: context, glucoseColor: hasStaticColorScheme ? .primary : glucoseColor)
+            } minimal: {
+                LiveActivityMinimalView(context: context, glucoseColor: glucoseColor)
             }
+            .widgetURL(URL(string: "Trio://"))
+            .keylineTint(glucoseColor)
+            .contentMargins(.horizontal, 0, for: .minimal)
+            .contentMargins(.trailing, 0, for: .compactLeading)
+            .contentMargins(.leading, 0, for: .compactTrailing)
         }
     }
+}
 
-    @ViewBuilder func content(context: ActivityViewContext<LiveActivityAttributes>) -> some View {
-        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
-        // TODO: grab target from proper targets, do not hard code.
-        let glucoseColor = getDynamicGlucoseColor(
-            glucoseValue: Decimal(string: context.state.bg) ?? 100,
-            highGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? context.state.highGlucose : context.state
-                .highGlucose.asMmolL,
-            lowGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? context.state.lowGlucose : context.state
-                .lowGlucose.asMmolL,
-            targetGlucose: context.state.detailedViewState?.unit == "mg/dL" ? 90 : 90.asMmolL,
-            glucoseColorScheme: context.state.glucoseColorScheme,
-            offset: context.state.detailedViewState?.unit == "mg/dL" ? 20 : 20.asMmolL
-        )
-
-        if let detailedViewState = context.state.detailedViewState {
-            HStack(spacing: 12) {
-                chart(context: context, additionalState: detailedViewState)
-                    .frame(maxWidth: UIScreen.main.bounds.width / 1.8)
-                VStack(alignment: .leading) {
-                    Spacer()
-                    bgLabel(context: context, additionalState: detailedViewState)
-                    HStack {
-                        changeLabel(context: context)
-                        trendArrow(context: context, additionalState: detailedViewState)
-                    }
-                    mealLabel(context: context, additionalState: detailedViewState).padding(.bottom, 8)
-                    updatedLabel(context: context).padding(.bottom, 10)
-                }
-            }
-            .privacySensitive()
-            .padding(.all, 14)
-            .imageScale(.small)
-            .foregroundColor(Color.white)
-            .activityBackgroundTint(Color.black.opacity(0.8))
-        } else {
-            Group {
-                if context.state.isInitialState {
-                    // add vertical and horizontal spacers around the label to ensure that the live activity view gets filled completely
-                    HStack {
-                        Spacer()
-                        VStack {
-                            Spacer()
-                            expiredLabel()
-                            Spacer()
-                        }
-                        Spacer()
-                    }
-                } else {
-                    HStack(spacing: 3) {
-                        bgAndTrend(
-                            context: context,
-                            size: .expanded,
-                            hasStaticColorScheme: hasStaticColorScheme,
-                            glucoseColor: glucoseColor
-                        ).0.font(.title)
-                        Spacer()
-                        VStack(alignment: .trailing, spacing: 5) {
-                            changeLabel(context: context).font(.title3)
-                                .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
-                            updatedLabel(context: context).font(.caption)
-                                .foregroundStyle(
-                                    hasStaticColorScheme ? .primary
-                                        .opacity(0.7) : glucoseColor
-                                )
-                        }
-                    }
-                }
-            }
-            .privacySensitive()
-            .padding(.all, 15)
-            // Semantic BackgroundStyle and Color values work here. They adapt to the given interface style (light mode, dark mode)
-            // Semantic UIColors do NOT (as of iOS 17.1.1). Like UIColor.systemBackgroundColor (it does not adapt to changes of the interface style)
-            // The colorScheme environment varaible that is usually used to detect dark mode does NOT work here (it reports false values)
-            .foregroundStyle(Color.primary)
-            .background(BackgroundStyle.background.opacity(0.4))
-            .activityBackgroundTint(Color.clear)
-        }
-    }
-
-    func dynamicIsland(context: ActivityViewContext<LiveActivityAttributes>) -> DynamicIsland {
-        let glucoseValueForColor = context.state.bg
-        let highGlucose = context.state.highGlucose
-        let lowGlucose = context.state.lowGlucose
-
-        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
-        // TODO: grab target from proper targets, do not hard code.
-        let glucoseColor = getDynamicGlucoseColor(
-            glucoseValue: Decimal(string: glucoseValueForColor) ?? 100,
-            highGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? highGlucose : highGlucose.asMmolL,
-            lowGlucoseColorValue: context.state.detailedViewState?.unit == "mg/dL" ? lowGlucose : lowGlucose.asMmolL,
-            targetGlucose: context.state.detailedViewState?.unit == "mg/dL" ? 90 : 90.asMmolL,
-            glucoseColorScheme: context.state.glucoseColorScheme,
-            offset: context.state.detailedViewState?.unit == "mg/dL" ? 20 : 20.asMmolL
-        )
-
-        return DynamicIsland {
-            DynamicIslandExpandedRegion(.leading) {
-                bgAndTrend(
-                    context: context,
-                    size: .expanded,
-                    hasStaticColorScheme: hasStaticColorScheme,
-                    glucoseColor: glucoseColor
-                ).0.font(.title2).padding(.leading, 5)
-            }
-            DynamicIslandExpandedRegion(.trailing) {
-                changeLabel(context: context).font(.title2).padding(.trailing, 5)
-                    .foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
-            }
-            DynamicIslandExpandedRegion(.bottom) {
-                if context.state.isInitialState {
-                    expiredLabel()
-                } else if let detailedViewState = context.state.detailedViewState {
-                    chart(context: context, additionalState: detailedViewState)
-                } else {
-                    Group {
-                        updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
-                    }
-                    .frame(
-                        maxHeight: .infinity,
-                        alignment: .bottom
-                    )
-                }
-            }
-            DynamicIslandExpandedRegion(.center) {
-                if context.state.detailedViewState != nil {
-                    updatedLabel(context: context).font(.caption).foregroundStyle(Color.secondary)
-                }
-            }
-        } compactLeading: {
-            bgAndTrend(context: context, size: .compact, hasStaticColorScheme: hasStaticColorScheme, glucoseColor: glucoseColor).0
-                .padding(.leading, 4)
-        } compactTrailing: {
-            changeLabel(context: context).padding(.trailing, 4).foregroundStyle(hasStaticColorScheme ? .primary : glucoseColor)
-        } minimal: {
-            let (_label, characterCount) = bgAndTrend(
-                context: context,
-                size: .minimal,
-                hasStaticColorScheme: hasStaticColorScheme,
-                glucoseColor: glucoseColor
-            )
-            let label = _label.padding(.leading, 7).padding(.trailing, 3)
-
-            if characterCount < 4 {
-                label
-            } else if characterCount < 5 {
-                label.fontWidth(.condensed)
-            } else {
-                label.fontWidth(.compressed)
-            }
-        }
-        .widgetURL(URL(string: "Trio://"))
-        .keylineTint(hasStaticColorScheme ? Color.purple : glucoseColor)
-        .contentMargins(.horizontal, 0, for: .minimal)
-        .contentMargins(.trailing, 0, for: .compactLeading)
-        .contentMargins(.leading, 0, for: .compactTrailing)
-    }
-
-    var body: some WidgetConfiguration {
-        ActivityConfiguration(for: LiveActivityAttributes.self, content: self.content, dynamicIsland: self.dynamicIsland)
-    }
+// Mock structure to replace GlucoseData
+struct MockGlucoseData {
+    var glucose: Int
+    var date: Date
+    var direction: String? // You can refine this based on your expected data
 }
 
 private extension LiveActivityAttributes {
@@ -582,17 +73,39 @@ private extension LiveActivityAttributes {
 }
 
 private extension LiveActivityAttributes.ContentState {
+    static var chartData: [MockGlucoseData] = [
+        MockGlucoseData(glucose: 120, date: Date().addingTimeInterval(-600), direction: "flat"),
+        MockGlucoseData(glucose: 125, date: Date().addingTimeInterval(-300), direction: "flat"),
+        MockGlucoseData(glucose: 130, date: Date(), direction: "flat")
+    ]
+
+    static var detailedViewState = LiveActivityAttributes.ContentAdditionalState(
+        chart: chartData.map { Decimal($0.glucose) },
+        chartDate: chartData.map(\.date),
+        rotationDegrees: 0,
+        cob: 20,
+        iob: 1.5,
+        unit: GlucoseUnits.mgdL.rawValue,
+        isOverrideActive: false,
+        overrideName: "Exercise",
+        overrideDate: Date().addingTimeInterval(-3600),
+        overrideDuration: 120,
+        overrideTarget: 150,
+        widgetItems: LiveActivityAttributes.LiveActivityItem.defaultItems
+    )
+
     // 0 is the widest digit. Use this to get an upper bound on text width.
 
     // Use mmol/l notation with decimal point as well for the same reason, it uses up to 4 characters, while mg/dl uses up to 3
     static var testWide: LiveActivityAttributes.ContentState {
         LiveActivityAttributes.ContentState(
-            bg: 00.0.description,
+            bg: "00.0",
             direction: "→",
             change: "+0.0",
             date: Date(),
             highGlucose: 180,
             lowGlucose: 70,
+            target: 100,
             glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
@@ -607,6 +120,7 @@ private extension LiveActivityAttributes.ContentState {
             date: Date(),
             highGlucose: 180,
             lowGlucose: 70,
+            target: 100,
             glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
@@ -621,6 +135,7 @@ private extension LiveActivityAttributes.ContentState {
             date: Date(),
             highGlucose: 180,
             lowGlucose: 70,
+            target: 100,
             glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
@@ -636,6 +151,7 @@ private extension LiveActivityAttributes.ContentState {
             date: Date(),
             highGlucose: 180,
             lowGlucose: 70,
+            target: 100,
             glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
@@ -650,6 +166,7 @@ private extension LiveActivityAttributes.ContentState {
             date: Date(),
             highGlucose: 180,
             lowGlucose: 70,
+            target: 100,
             glucoseColorScheme: "staticColor",
             detailedViewState: nil,
             isInitialState: false
@@ -664,15 +181,107 @@ private extension LiveActivityAttributes.ContentState {
             date: Date().addingTimeInterval(-60 * 60),
             highGlucose: 180,
             lowGlucose: 70,
+            target: 100,
             glucoseColorScheme: "staticColor",
             detailedViewState: nil,
-            isInitialState: true
+            isInitialState: false
+        )
+    }
+
+    static var testWideDetailed: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(
+            bg: "00.0",
+            direction: "→",
+            change: "+0.0",
+            date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            target: 100,
+            glucoseColorScheme: "staticColor",
+            detailedViewState: detailedViewState,
+            isInitialState: false
+        )
+    }
+
+    static var testVeryWideDetailed: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(
+            bg: "00.0",
+            direction: "↑↑",
+            change: "+0.0",
+            date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            target: 100,
+            glucoseColorScheme: "staticColor",
+            detailedViewState: detailedViewState,
+            isInitialState: false
+        )
+    }
+
+    static var testSuperWideDetailed: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(
+            bg: "00.0",
+            direction: "↑↑↑",
+            change: "+0.0",
+            date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            target: 100,
+            glucoseColorScheme: "staticColor",
+            detailedViewState: detailedViewState,
+            isInitialState: false
+        )
+    }
+
+    // 2 characters for BG, 1 character for change is the minimum that will be shown
+    static var testNarrowDetailed: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(
+            bg: "00",
+            direction: "↑",
+            change: "+0",
+            date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            target: 100,
+            glucoseColorScheme: "staticColor",
+            detailedViewState: detailedViewState,
+            isInitialState: false
+        )
+    }
+
+    static var testMediumDetailed: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(
+            bg: "000",
+            direction: "↗︎",
+            change: "+00",
+            date: Date(),
+            highGlucose: 180,
+            lowGlucose: 70,
+            target: 100,
+            glucoseColorScheme: "staticColor",
+            detailedViewState: detailedViewState,
+            isInitialState: false
+        )
+    }
+
+    static var testExpiredDetailed: LiveActivityAttributes.ContentState {
+        LiveActivityAttributes.ContentState(
+            bg: "--",
+            direction: nil,
+            change: "--",
+            date: Date().addingTimeInterval(-60 * 60),
+            highGlucose: 180,
+            lowGlucose: 70,
+            target: 100,
+            glucoseColorScheme: "staticColor",
+            detailedViewState: detailedViewState,
+            isInitialState: false
         )
     }
 }
 
 @available(iOS 17.0, iOSApplicationExtension 17.0, *)
-#Preview("Notification", as: .content, using: LiveActivityAttributes.preview) {
+#Preview("Simple", as: .content, using: LiveActivityAttributes.preview) {
     LiveActivity()
 } contentStates: {
     LiveActivityAttributes.ContentState.testSuperWide
@@ -682,3 +291,15 @@ private extension LiveActivityAttributes.ContentState {
     LiveActivityAttributes.ContentState.testNarrow
     LiveActivityAttributes.ContentState.testExpired
 }
+
+@available(iOS 17.0, iOSApplicationExtension 17.0, *)
+#Preview("Detailed", as: .content, using: LiveActivityAttributes.preview) {
+    LiveActivity()
+} contentStates: {
+    LiveActivityAttributes.ContentState.testSuperWideDetailed
+    LiveActivityAttributes.ContentState.testVeryWideDetailed
+    LiveActivityAttributes.ContentState.testWideDetailed
+    LiveActivityAttributes.ContentState.testMediumDetailed
+    LiveActivityAttributes.ContentState.testNarrowDetailed
+    LiveActivityAttributes.ContentState.testExpiredDetailed
+}

+ 20 - 0
LiveActivity/Views/LiveActivityBGAndTrendView.swift

@@ -0,0 +1,20 @@
+//
+//  LiveActivityBGAndTrendView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityBGAndTrendView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var size: Size
+    var glucoseColor: Color
+
+    var body: some View {
+        let (view, _) = bgAndTrend(context: context, size: size, glucoseColor: glucoseColor)
+        return view
+    }
+}

+ 152 - 0
LiveActivity/Views/LiveActivityChartView.swift

@@ -0,0 +1,152 @@
+//
+//  LiveActivityChartView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Charts
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityChartView: View {
+    @Environment(\.colorScheme) var colorScheme
+
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    var body: some View {
+        let state = context.state
+        let isMgdL: Bool = additionalState.unit == "mg/dL"
+
+        // Determine scale
+        let minValue = min(additionalState.chart.min() ?? 39, 39) as Decimal
+        let maxValue = max(additionalState.chart.max() ?? 300, 300) as Decimal
+
+        let yAxisRuleMarkMin = isMgdL ? state.lowGlucose : state.lowGlucose
+            .asMmolL
+        let yAxisRuleMarkMax = isMgdL ? state.highGlucose : state.highGlucose
+            .asMmolL
+        let target = isMgdL ? state.target : state.target.asMmolL
+
+        let isOverrideActive = additionalState.isOverrideActive == true
+
+        let calendar = Calendar.current
+        let now = Date()
+
+        let startDate = calendar.date(byAdding: .hour, value: -6, to: now) ?? now
+        let endDate = isOverrideActive ? (calendar.date(byAdding: .hour, value: 2, to: now) ?? now) : now
+
+        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+        let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
+        let hardCodedHigh = isMgdL ? Decimal(220) : 220.asMmolL
+        let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+
+        let highColor = Color.getDynamicGlucoseColor(
+            glucoseValue: yAxisRuleMarkMax,
+            highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : yAxisRuleMarkMax,
+            lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : yAxisRuleMarkMin,
+            targetGlucose: target,
+            glucoseColorScheme: context.state.glucoseColorScheme
+        )
+
+        let lowColor = Color.getDynamicGlucoseColor(
+            glucoseValue: yAxisRuleMarkMin,
+            highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : yAxisRuleMarkMax,
+            lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : yAxisRuleMarkMin,
+            targetGlucose: target,
+            glucoseColorScheme: context.state.glucoseColorScheme
+        )
+
+        Chart {
+            RuleMark(y: .value("High", yAxisRuleMarkMax))
+                .foregroundStyle(highColor)
+                .lineStyle(.init(lineWidth: 1, dash: [5]))
+
+            RuleMark(y: .value("Low", yAxisRuleMarkMin))
+                .foregroundStyle(lowColor)
+                .lineStyle(.init(lineWidth: 1, dash: [5]))
+
+            RuleMark(y: .value("Target", target))
+                .foregroundStyle(.green.gradient)
+                .lineStyle(.init(lineWidth: 1.5))
+
+            if isOverrideActive {
+                drawActiveOverrides()
+            }
+
+            drawChart(yAxisRuleMarkMin: yAxisRuleMarkMin, yAxisRuleMarkMax: yAxisRuleMarkMax)
+        }
+        .chartYAxis {
+            AxisMarks(position: .trailing) { _ in
+                AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
+                    .foregroundStyle(Color.white.opacity(colorScheme == .light ? 1 : 0.5))
+                AxisValueLabel().foregroundStyle(.primary).font(.footnote)
+            }
+        }
+        .chartYScale(domain: additionalState.unit == "mg/dL" ? minValue ... maxValue : minValue.asMmolL ... maxValue.asMmolL)
+        .chartYAxis(.hidden)
+        .chartPlotStyle { plotContent in
+            plotContent
+                .background(
+                    RoundedRectangle(cornerRadius: 12)
+                        .fill(colorScheme == .light ? Color.black.opacity(0.2) : .clear)
+                )
+                .clipShape(RoundedRectangle(cornerRadius: 12))
+        }
+        .chartXScale(domain: startDate ... endDate)
+        .chartXAxis {
+            AxisMarks(position: .automatic) { _ in
+                AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
+                    .foregroundStyle(Color.primary.opacity(colorScheme == .light ? 1 : 0.5))
+            }
+        }
+    }
+
+    private func drawActiveOverrides() -> some ChartContent {
+        let start: Date = context.state.detailedViewState?.overrideDate ?? .distantPast
+
+        let duration = context.state.detailedViewState?.overrideDuration ?? 0
+        let durationAsTimeInterval = TimeInterval((duration as NSDecimalNumber).doubleValue * 60) // return seconds
+
+        let end: Date = start.addingTimeInterval(durationAsTimeInterval)
+        let target = context.state.detailedViewState?.overrideTarget ?? 0
+
+        return RuleMark(
+            xStart: .value("Start", start, unit: .second),
+            xEnd: .value("End", end, unit: .second),
+            y: .value("Value", target)
+        )
+        .foregroundStyle(Color.purple.opacity(0.6))
+        .lineStyle(.init(lineWidth: 8))
+    }
+
+    private func drawChart(yAxisRuleMarkMin _: Decimal, yAxisRuleMarkMax _: Decimal) -> some ChartContent {
+        ForEach(additionalState.chart.indices, id: \.self) { index in
+            let isMgdL = additionalState.unit == "mg/dL"
+            let currentValue = additionalState.chart[index]
+            let displayValue = isMgdL ? currentValue : currentValue.asMmolL
+            let chartDate = additionalState.chartDate[index] ?? Date()
+
+            // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+            let hardCodedLow = Decimal(55)
+            let hardCodedHigh = Decimal(220)
+            let hasStaticColorScheme = context.state.glucoseColorScheme == "staticColor"
+
+            let pointMarkColor = Color.getDynamicGlucoseColor(
+                glucoseValue: currentValue,
+                highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : context.state.highGlucose,
+                lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : context.state.lowGlucose,
+                targetGlucose: context.state.target,
+                glucoseColorScheme: context.state.glucoseColorScheme
+            )
+
+            let pointMark = PointMark(
+                x: .value("Time", chartDate),
+                y: .value("Value", displayValue)
+            ).symbolSize(16)
+
+            pointMark.foregroundStyle(pointMarkColor)
+        }
+    }
+}

+ 25 - 0
LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift

@@ -0,0 +1,25 @@
+//
+//  LiveActivityGlucoseDeltaLabelView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityGlucoseDeltaLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+    var isDetailed: Bool = false
+
+    var body: some View {
+        if !context.state.change.isEmpty {
+            Text(context.state.change)
+                .foregroundStyle(context.state.glucoseColorScheme == "staticColor" ? .primary : glucoseColor)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+        } else {
+            Text("--")
+        }
+    }
+}

+ 222 - 0
LiveActivity/Views/LiveActivityView.swift

@@ -0,0 +1,222 @@
+//
+//  LiveActivityView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import ActivityKit
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityView: View {
+    @Environment(\.colorScheme) var colorScheme
+    var context: ActivityViewContext<LiveActivityAttributes>
+
+    private var hasStaticColorScheme: Bool {
+        context.state.glucoseColorScheme == "staticColor"
+    }
+
+    private var glucoseColor: Color {
+        let state = context.state
+        let detailedState = state.detailedViewState
+        let isMgdL = detailedState?.unit == "mg/dL"
+
+        // TODO: workaround for now: set low value to 55, to have dynamic color shades between 55 and user-set low (approx. 70); same for high glucose
+        let hardCodedLow = isMgdL ? Decimal(55) : 55.asMmolL
+        let hardCodedHigh = isMgdL ? Decimal(220) : 220.asMmolL
+
+        return Color.getDynamicGlucoseColor(
+            glucoseValue: Decimal(string: state.bg) ?? 100,
+            highGlucoseColorValue: !hasStaticColorScheme ? hardCodedHigh : state.highGlucose,
+            lowGlucoseColorValue: !hasStaticColorScheme ? hardCodedLow : state.lowGlucose,
+            targetGlucose: isMgdL ? state.target : state.target.asMmolL,
+            glucoseColorScheme: state.glucoseColorScheme
+        )
+    }
+
+    var body: some View {
+        if let detailedViewState = context.state.detailedViewState {
+            VStack {
+                LiveActivityChartView(context: context, additionalState: detailedViewState)
+                    .frame(maxWidth: UIScreen.main.bounds.width * 0.9)
+                    .frame(height: 80)
+                    .overlay(alignment: .topTrailing) {
+                        if detailedViewState.isOverrideActive {
+                            HStack {
+                                Text("\(detailedViewState.overrideName)")
+                                    .font(.footnote)
+                                    .fontWeight(.bold)
+                                    .foregroundStyle(.white)
+                            }
+                            .padding(6)
+                            .background {
+                                RoundedRectangle(cornerRadius: 10)
+                                    .fill(Color.purple.opacity(colorScheme == .dark ? 0.6 : 0.8))
+                            }
+                        }
+                    }
+
+                HStack {
+                    if detailedViewState.widgetItems.contains(where: { $0 != .empty }) {
+                        ForEach(Array(detailedViewState.widgetItems.enumerated()), id: \.element) { index, widgetItem in
+                            switch widgetItem {
+                            case .currentGlucose:
+                                VStack {
+                                    LiveActivityBGLabelView(context: context, additionalState: detailedViewState)
+                                    HStack {
+                                        LiveActivityGlucoseDeltaLabelView(
+                                            context: context,
+                                            glucoseColor: .primary,
+                                            isDetailed: true
+                                        )
+                                        if !context.isStale, let direction = context.state.direction {
+                                            Text(direction).font(.headline)
+                                        }
+                                    }
+                                }
+                            case .iob:
+                                LiveActivityIOBLabelView(context: context, additionalState: detailedViewState)
+                            case .cob:
+                                LiveActivityCOBLabelView(context: context, additionalState: detailedViewState)
+                            case .updatedLabel:
+                                LiveActivityUpdatedLabelView(context: context, isDetailedLayout: true)
+                            case .empty:
+                                Text("").frame(width: 50, height: 50)
+                            }
+
+                            /// Check if the next item is also non-empty to determine if a divider should be shown
+                            if index < detailedViewState.widgetItems.count - 1 {
+                                let currentItem = detailedViewState.widgetItems[index]
+                                let nextItem = detailedViewState.widgetItems[index + 1]
+
+                                if currentItem != .empty, nextItem != .empty {
+                                    Divider()
+                                        .foregroundStyle(.primary)
+                                        .fontWeight(.bold)
+                                        .frame(width: 10)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            .privacySensitive()
+            .padding(.all, 14)
+            .foregroundStyle(Color.primary)
+            // Semantic BackgroundStyle and Color values work here. They adapt to the given interface style (light mode, dark mode)
+            // Semantic UIColors do NOT (as of iOS 17.1.1). Like UIColor.systemBackgroundColor (it does not adapt to changes of the interface style)
+            // The colorScheme environment variable does work here, but BackgroundStyle gives us this functionality for free
+            .foregroundStyle(Color.primary)
+            .background(BackgroundStyle.background.opacity(0.4))
+            .activityBackgroundTint(Color.clear)
+        } else {
+            Group {
+                if context.state.isInitialState {
+                    Text("Live Activity Expired. Open Trio to Refresh").minimumScaleFactor(0.01)
+                } else {
+                    HStack(spacing: 3) {
+                        LiveActivityBGAndTrendView(context: context, size: .expanded, glucoseColor: glucoseColor).font(.title)
+                        Spacer()
+                        VStack(alignment: .trailing, spacing: 5) {
+                            LiveActivityGlucoseDeltaLabelView(
+                                context: context,
+                                glucoseColor: hasStaticColorScheme ? .primary : glucoseColor,
+                                isDetailed: false
+                            ).font(.title3)
+                            LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(.caption)
+                                .foregroundStyle(.primary.opacity(0.7))
+                        }
+                    }
+                }
+            }
+            .privacySensitive()
+            .padding(.all, 15)
+            .foregroundStyle(Color.primary)
+            /// Semantic BackgroundStyle and Color values work here. They adapt to the given interface style (light mode, dark mode)
+            // Semantic UIColors do NOT (as of iOS 17.1.1). Like UIColor.systemBackgroundColor (it does not adapt to changes of the interface style)
+            // The colorScheme environment variable does work here, but BackgroundStyle gives us this functionality for free
+            .foregroundStyle(Color.primary)
+            .background(BackgroundStyle.background.opacity(0.4))
+            .activityBackgroundTint(Color.clear)
+        }
+    }
+}
+
+// Expanded, minimal, compact view components
+struct LiveActivityExpandedLeadingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+
+    var body: some View {
+        LiveActivityBGAndTrendView(context: context, size: .expanded, glucoseColor: glucoseColor).font(.title2)
+            .padding(.leading, 5)
+    }
+}
+
+struct LiveActivityExpandedTrailingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+
+    var body: some View {
+        LiveActivityGlucoseDeltaLabelView(context: context, glucoseColor: glucoseColor, isDetailed: false).font(.title2)
+            .padding(.trailing, 5)
+    }
+}
+
+struct LiveActivityExpandedBottomView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+
+    var body: some View {
+        if context.state.isInitialState {
+            Text("Live Activity Expired. Open Trio to Refresh").minimumScaleFactor(0.01)
+        } else if let detailedViewState = context.state.detailedViewState {
+            LiveActivityChartView(context: context, additionalState: detailedViewState)
+        }
+    }
+}
+
+struct LiveActivityExpandedCenterView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+
+    var body: some View {
+        LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(Font.caption)
+            .foregroundStyle(Color.secondary)
+    }
+}
+
+struct LiveActivityCompactLeadingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+
+    var body: some View {
+        LiveActivityBGAndTrendView(context: context, size: .compact, glucoseColor: glucoseColor).padding(.leading, 4)
+    }
+}
+
+struct LiveActivityCompactTrailingView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+
+    var body: some View {
+        LiveActivityGlucoseDeltaLabelView(context: context, glucoseColor: glucoseColor, isDetailed: false).padding(.trailing, 4)
+    }
+}
+
+struct LiveActivityMinimalView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var glucoseColor: Color
+
+    var body: some View {
+        let (label, characterCount) = bgAndTrend(context: context, size: .minimal, glucoseColor: glucoseColor)
+        let adjustedLabel = label.padding(.leading, 5).padding(.trailing, 2)
+
+        if characterCount < 4 {
+            adjustedLabel.fontWidth(.condensed)
+        } else if characterCount < 5 {
+            adjustedLabel.fontWidth(.compressed)
+        } else {
+            adjustedLabel.fontWidth(.compressed)
+        }
+    }
+}

+ 22 - 0
LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift

@@ -0,0 +1,22 @@
+//
+//  LiveActivityBGLabelView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityBGLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    var body: some View {
+        Text(context.state.bg)
+            .fontWeight(.bold)
+            .font(.title3)
+            .foregroundStyle(context.isStale ? .secondary : .primary)
+            .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+    }
+}

+ 33 - 0
LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift

@@ -0,0 +1,33 @@
+//
+//  LiveActivityCOBLabelView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityCOBLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    var body: some View {
+        VStack(spacing: 2) {
+            HStack {
+                Text(
+                    "\(additionalState.cob)"
+                ).fontWeight(.bold)
+                    .font(.title3)
+                    .foregroundStyle(context.isStale ? .secondary : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+                Text("g")
+                    .font(.headline).fontWeight(.bold)
+                    .foregroundStyle(context.isStale ? .secondary : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            }
+            Text("COB").font(.subheadline).foregroundStyle(.primary)
+        }
+    }
+}

+ 42 - 0
LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift

@@ -0,0 +1,42 @@
+//
+//  LiveActivityWidgetItems.swift
+//  LiveActivityExtension
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityIOBLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    private var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        formatter.decimalSeparator = "."
+        return formatter
+    }
+
+    var body: some View {
+        VStack(spacing: 2) {
+            HStack {
+                Text(
+                    bolusFormatter.string(from: additionalState.iob as NSNumber) ?? "--"
+                )
+                .fontWeight(.bold)
+                .font(.title3)
+                .foregroundStyle(context.isStale ? .secondary : .primary)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+                Text("U")
+                    .font(.headline).fontWeight(.bold)
+                    .foregroundStyle(context.isStale ? .secondary : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            }
+            Text("IOB").font(.subheadline).foregroundStyle(.primary)
+        }
+    }
+}

+ 47 - 0
LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift

@@ -0,0 +1,47 @@
+//
+//  LiveActivityUpdatedLabelView.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 17.10.24.
+//
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityUpdatedLabelView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var isDetailedLayout: Bool
+
+    private var dateFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        return formatter
+    }
+
+    var body: some View {
+        let dateText = Text("\(dateFormatter.string(from: context.state.date))")
+
+        if isDetailedLayout {
+            VStack {
+                dateText
+                    .font(.title3)
+                    .bold()
+                    .foregroundStyle(context.isStale ? .red.opacity(0.6) : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+                Text("Updated").font(.subheadline).foregroundStyle(.primary)
+            }
+        } else {
+            HStack {
+                Text("Updated:").font(.subheadline).foregroundStyle(.secondary)
+
+                dateText
+                    .font(.subheadline)
+                    .bold()
+                    .foregroundStyle(context.isStale ? .red.opacity(0.6) : .secondary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            }
+        }
+    }
+}

+ 1 - 0
Model/Helper/CustomNotification.swift

@@ -4,6 +4,7 @@ import Foundation
 extension Notification.Name {
     static let willUpdateOverrideConfiguration = Notification.Name("willUpdateOverrideConfiguration")
     static let didUpdateOverrideConfiguration = Notification.Name("didUpdateOverrideConfiguration")
+    static let liveActivityOrderDidChange = Notification.Name("liveActivityOrderDidChange")
 }
 
 func awaitNotification(_ name: Notification.Name) async {