Bläddra i källkod

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Trio-dev into remote_control

Deniz Cengiz 1 år sedan
förälder
incheckning
9636505f3e
37 ändrade filer med 1592 tillägg och 713 borttagningar
  1. 1 4
      .github/workflows/add_identifiers.yml
  2. 2 2
      .github/workflows/build_trio.yml
  3. 1 4
      .github/workflows/create_certs.yml
  4. 3 3
      .github/workflows/validate_secrets.yml
  5. 17 1
      FreeAPS.xcodeproj/project.pbxproj
  6. 3 4
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  7. 8 2
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  8. 3 3
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  9. 8 8
      FreeAPS/Sources/Modules/Bolus/View/ForecastChart.swift
  10. 2 2
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/GlucoseNotificationSettingsStateModel.swift
  11. 6 6
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  12. 16 12
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  13. 7 7
      FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift
  14. 10 7
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  15. 9 32
      FreeAPS/Sources/Modules/Home/View/Header/CurrentGlucoseView.swift
  16. 0 3
      FreeAPS/Sources/Modules/LiveActivitySettings/LiveActivitySettingsStateModel.swift
  17. 12 0
      FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift
  18. 388 0
      FreeAPS/Sources/Modules/LiveActivitySettings/View/LiveActivityWidgetConfiguration.swift
  19. 3 0
      FreeAPS/Sources/Router/Screen.swift
  20. 23 16
      FreeAPS/Sources/Services/LiveActivity/Data/DataManager.swift
  21. 1 0
      FreeAPS/Sources/Services/LiveActivity/Data/DeterminationData.swift
  22. 4 0
      FreeAPS/Sources/Services/LiveActivity/Data/OverrideData.swift
  23. 18 2
      FreeAPS/Sources/Services/LiveActivity/LiveActitiyAttributes.swift
  24. 28 2
      FreeAPS/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift
  25. 78 30
      FreeAPS/Sources/Services/LiveActivity/LiveActivityBridge.swift
  26. 1 1
      FreeAPS/Sources/Services/Network/TidepoolManager.swift
  27. 193 0
      LiveActivity/LiveActivity+Helper.swift
  28. 183 562
      LiveActivity/LiveActivity.swift
  29. 20 0
      LiveActivity/Views/LiveActivityBGAndTrendView.swift
  30. 152 0
      LiveActivity/Views/LiveActivityChartView.swift
  31. 25 0
      LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift
  32. 222 0
      LiveActivity/Views/LiveActivityView.swift
  33. 22 0
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift
  34. 33 0
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  35. 42 0
      LiveActivity/Views/WidgetItems/LiveActivityIOBLabelView.swift
  36. 47 0
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  37. 1 0
      Model/Helper/CustomNotification.swift

+ 1 - 4
.github/workflows/add_identifiers.yml

@@ -12,11 +12,8 @@ jobs:
   identifiers:
     name: Add Identifiers
     needs: validate
-    runs-on: macos-14
+    runs-on: macos-15
     steps:
-      # Uncomment to manually select latest Xcode if needed
-      #- name: Select Latest Xcode
-      #  run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer"
 
       # Checks-out the repo
       - name: Checkout Repo

+ 2 - 2
.github/workflows/build_trio.yml

@@ -186,7 +186,7 @@ jobs:
   build:
     name: Build
     needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
-    runs-on: macos-14
+    runs-on: macos-15
     permissions:
       contents: write
     if:
@@ -198,7 +198,7 @@ jobs:
       )
     steps:
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_15.4.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_16.0.app/Contents/Developer"
       
       - name: Checkout Repo for syncing
         if: |

+ 1 - 4
.github/workflows/create_certs.yml

@@ -12,11 +12,8 @@ jobs:
   certificates:
     name: Create Certificates
     needs: validate
-    runs-on: macos-14
+    runs-on: macos-15
     steps:
-      # Uncomment to manually select latest Xcode if needed
-      #- name: Select Latest Xcode
-      #  run: "sudo xcode-select --switch /Applications/Xcode_13.0.app/Contents/Developer"
 
       # Checks-out the repo
       - name: Checkout Repo

+ 3 - 3
.github/workflows/validate_secrets.yml

@@ -5,7 +5,7 @@ on: [workflow_call, workflow_dispatch]
 jobs:
   validate-access-token:
     name: Access
-    runs-on: macos-14
+    runs-on: macos-15
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}
@@ -74,7 +74,7 @@ jobs:
   validate-match-secrets:
     name: Match-Secrets
     needs: validate-access-token
-    runs-on: macos-14
+    runs-on: macos-15
     env:
       GH_TOKEN: ${{ secrets.GH_PAT }}
     steps:
@@ -112,7 +112,7 @@ jobs:
   validate-fastlane-secrets:
     name: Fastlane
     needs: [validate-access-token, validate-match-secrets]
-    runs-on: macos-14
+    runs-on: macos-15
     env:
       GH_PAT: ${{ secrets.GH_PAT }}
       GH_TOKEN: ${{ secrets.GH_PAT }}

+ 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 */; };
@@ -457,6 +458,7 @@
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
+		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 */; };
@@ -990,6 +992,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>"; };
@@ -1128,6 +1131,7 @@
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
+		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>"; };
@@ -1199,6 +1203,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;
@@ -2336,10 +2344,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>";
@@ -2812,6 +2822,7 @@
 			isa = PBXGroup;
 			children = (
 				DDF847E32C5C288F0049BB3B /* LiveActivitySettingsRootView.swift */,
+				BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -3009,6 +3020,9 @@
 			);
 			dependencies = (
 			);
+			fileSystemSynchronizedGroups = (
+				DDCEBF412CC1B42500DF4C36 /* Views */,
+			);
 			name = LiveActivityExtension;
 			productName = LiveActivityExtension;
 			productReference = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */;
@@ -3539,6 +3553,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 */,
@@ -3724,6 +3739,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
     }
 }

+ 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

@@ -115,6 +115,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 remoteControlConfig
@@ -133,6 +134,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 {