Pārlūkot izejas kodu

Merge branch 'core-data-sync-trio' of github.com:dnzxy/Open-iAPS into auggie-dynamic-bg-color

Deniz Cengiz 1 gadu atpakaļ
vecāks
revīzija
7ed2e51792
100 mainītis faili ar 3393 papildinājumiem un 1347 dzēšanām
  1. 684 341
      FreeAPS.xcodeproj/project.pbxproj
  2. 16 0
      FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme
  3. 10 11
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  4. 4 4
      FreeAPS/Resources/json/defaults/settings/bg_targets.json
  5. 3 3
      FreeAPS/Resources/json/defaults/settings/insulin_sensitivities.json
  6. 16 6
      FreeAPS/Sources/APS/APSManager.swift
  7. 1 36
      FreeAPS/Sources/APS/CGM/CGMType.swift
  8. 37 0
      FreeAPS/Sources/APS/DeviceDataManager.swift
  9. 0 2
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  10. 1 1
      FreeAPS/Sources/APS/FetchTreatmentsManager.swift
  11. 26 25
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  12. 2 2
      FreeAPS/Sources/APS/PluginManager.swift
  13. 108 18
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  14. 38 6
      FreeAPS/Sources/APS/Storage/DeterminationStorage.swift
  15. 13 7
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  16. 10 10
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  17. 15 3
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  18. 15 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  19. 2 3
      FreeAPS/Sources/Helpers/DynamicGlucoseColor.swift
  20. 99 0
      FreeAPS/Sources/Helpers/MainChartHelper.swift
  21. 1 1
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  22. 1 1
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  23. 1 1
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  24. 1 1
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  25. 1 1
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  26. 1 1
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  27. 1 1
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  28. 1 1
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  29. 1 1
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  30. 1 1
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  31. 1 1
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  32. 1 1
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  33. 1 1
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  34. 1 1
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  35. 1 1
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  36. 1 1
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  37. 1 1
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  38. 1 1
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  39. 1 1
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  40. 1 1
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  41. 1 1
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  42. 1 1
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  43. 1 1
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  44. 4 4
      FreeAPS/Sources/Models/BGTargets.swift
  45. 0 10
      FreeAPS/Sources/Models/BloodGlucose.swift
  46. 15 0
      FreeAPS/Sources/Models/ColorSchemeOption.swift
  47. 158 0
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  48. 10 10
      FreeAPS/Sources/Models/Determination.swift
  49. 16 0
      FreeAPS/Sources/Models/ForecastDisplayType.swift
  50. 12 32
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  51. 22 0
      FreeAPS/Sources/Models/GlucoseColorStyle.swift
  52. 0 16
      FreeAPS/Sources/Models/HistoryLayout.swift
  53. 4 4
      FreeAPS/Sources/Models/InsulinSensitivities.swift
  54. 0 6
      FreeAPS/Sources/Models/NightscoutStatistics.swift
  55. 1 1
      FreeAPS/Sources/Models/Preferences.swift
  56. 1 6
      FreeAPS/Sources/Models/RawFetchedProfile.swift
  57. 22 0
      FreeAPS/Sources/Models/TotalInsulinDisplayType.swift
  58. 5 0
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsDataFlow.swift
  59. 64 0
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsProvider.swift
  60. 121 0
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  61. 325 0
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  62. 5 0
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsDataFlow.swift
  63. 3 0
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsProvider.swift
  64. 51 0
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  65. 119 0
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  66. 6 0
      FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigStateModel.swift
  67. 84 20
      FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift
  68. 4 3
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorDataFlow.swift
  69. 2 2
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorProvider.swift
  70. 39 8
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  71. 64 45
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  72. 2 2
      FreeAPS/Sources/Modules/Bolus/BolusProvider.swift
  73. 115 85
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  74. 57 66
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  75. 57 65
      FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift
  76. 45 58
      FreeAPS/Sources/Modules/Bolus/View/PopupView.swift
  77. 9 4
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift
  78. 84 60
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  79. 29 42
      FreeAPS/Sources/Modules/CGM/CGMStateModel.swift
  80. 128 64
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  81. 5 0
      FreeAPS/Sources/Modules/CalendarEventSettings/CalendarEventSettingsDataFlow.swift
  82. 3 0
      FreeAPS/Sources/Modules/CalendarEventSettings/CalendarEventSettingsProvider.swift
  83. 65 0
      FreeAPS/Sources/Modules/CalendarEventSettings/CalendarEventSettingsStateModel.swift
  84. 132 0
      FreeAPS/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift
  85. 4 4
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  86. 4 3
      FreeAPS/Sources/Modules/CREditor/CREditorDataFlow.swift
  87. 2 2
      FreeAPS/Sources/Modules/CREditor/CREditorProvider.swift
  88. 31 2
      FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift
  89. 66 49
      FreeAPS/Sources/Modules/CREditor/View/CREditorRootView.swift
  90. 8 25
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  91. 15 3
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  92. 0 5
      FreeAPS/Sources/Modules/Dynamic/DynamicDataFlow.swift
  93. 0 3
      FreeAPS/Sources/Modules/Dynamic/DynamicProvider.swift
  94. 0 112
      FreeAPS/Sources/Modules/Dynamic/View/DynamicRootView.swift
  95. 5 0
      FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsDataFlow.swift
  96. 3 0
      FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsProvider.swift
  97. 18 20
      FreeAPS/Sources/Modules/Dynamic/DynamicStateModel.swift
  98. 226 0
      FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  99. 0 5
      FreeAPS/Sources/Modules/FPUConfig/FPUConfigDataFlow.swift
  100. 0 0
      FreeAPS/Sources/Modules/FPUConfig/FPUConfigProvider.swift

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 684 - 341
FreeAPS.xcodeproj/project.pbxproj


+ 16 - 0
FreeAPS.xcodeproj/xcshareddata/xcschemes/Trio.xcscheme

@@ -348,6 +348,8 @@
       buildConfiguration = "Debug"
       buildConfiguration = "Debug"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      enableThreadSanitizer = "YES"
+      enableUBSanitizer = "YES"
       launchStyle = "0"
       launchStyle = "0"
       useCustomWorkingDirectory = "NO"
       useCustomWorkingDirectory = "NO"
       ignoresPersistentStateOnLaunch = "NO"
       ignoresPersistentStateOnLaunch = "NO"
@@ -364,6 +366,20 @@
             ReferencedContainer = "container:FreeAPS.xcodeproj">
             ReferencedContainer = "container:FreeAPS.xcodeproj">
          </BuildableReference>
          </BuildableReference>
       </BuildableProductRunnable>
       </BuildableProductRunnable>
+      <CommandLineArguments>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.ConcurrencyDebug 1"
+            isEnabled = "YES">
+         </CommandLineArgument>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.SQLDebug 1"
+            isEnabled = "NO">
+         </CommandLineArgument>
+         <CommandLineArgument
+            argument = "-com.apple.CoreData.MigrationDebug 1"
+            isEnabled = "NO">
+         </CommandLineArgument>
+      </CommandLineArguments>
       <EnvironmentVariables>
       <EnvironmentVariables>
          <EnvironmentVariable
          <EnvironmentVariable
             key = "CG_NUMERICS_SHOW_BACKTRACE"
             key = "CG_NUMERICS_SHOW_BACKTRACE"

+ 10 - 11
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -1,5 +1,5 @@
 {
 {
-  "units" : "mmol/L",
+  "units" : "mg/dL",
   "closedLoop" : false,
   "closedLoop" : false,
   "allowAnnouncements" : false,
   "allowAnnouncements" : false,
   "useAutotune" : false,
   "useAutotune" : false,
@@ -9,8 +9,6 @@
   "useLocalGlucoseSource" : false,
   "useLocalGlucoseSource" : false,
   "localGlucosePort" : 8080,
   "localGlucosePort" : 8080,
   "debugOptions" : false,
   "debugOptions" : false,
-  "insulinReqPercentage" : 70,
-  "skipBolusScreenAfterCarbs" : false,
   "displayHR" : false,
   "displayHR" : false,
   "cgm" : "none",
   "cgm" : "none",
   "cgmManagerTypeByIdentifier":"",
   "cgmManagerTypeByIdentifier":"",
@@ -25,9 +23,9 @@
   "lowGlucose" : 72,
   "lowGlucose" : 72,
   "highGlucose" : 270,
   "highGlucose" : 270,
   "carbsRequiredThreshold" : 10,
   "carbsRequiredThreshold" : 10,
-  "animatedBackground" : false,
+  "showCarbsRequiredBadge" : true,
   "useFPUconversion" : true,
   "useFPUconversion" : true,
-  "tins": false,
+  "totalInsulinDisplayType": "totalDailyDose",
   "individualAdjustmentFactor" : 0.5,
   "individualAdjustmentFactor" : 0.5,
   "timeCap" : 8,
   "timeCap" : 8,
   "minuteInterval" : 30,
   "minuteInterval" : 30,
@@ -39,23 +37,24 @@
   "high" : 180,
   "high" : 180,
   "low" : 70,
   "low" : 70,
   "hours" : 6,
   "hours" : 6,
-  "dynamicGlucoseColor" : false,
+  "glucoseColorStyle" : "staticColor",
   "xGridLines" : true,
   "xGridLines" : true,
   "yGridLines" : true,
   "yGridLines" : true,
   "oneDimensionalGraph" : false,
   "oneDimensionalGraph" : false,
   "rulerMarks" : true,
   "rulerMarks" : true,
-  "displayForecastsAsLines": false,
+  "forecastDisplayType": "cone",
   "maxCarbs": 250,
   "maxCarbs": 250,
   "maxFat": 250,
   "maxFat": 250,
   "maxProtein": 250,
   "maxProtein": 250,
   "displayFatAndProteinOnWatch": false,
   "displayFatAndProteinOnWatch": false,
   "confirmBolusFaster": false,
   "confirmBolusFaster": false,
   "overrideFactor": 0.8,
   "overrideFactor": 0.8,
-  "useCalc": true,
   "fattyMeals": false,
   "fattyMeals": false,
   "fattyMealFactor": 0.7,
   "fattyMealFactor": 0.7,
   "sweetMeals": false,
   "sweetMeals": false,
-  "sweetMealFactor": 2
-  "historyLayout": "twoTabs",
-  "lockScreenView": "simple"
+  "sweetMealFactor": 2,
+  "lockScreenView": "simple",
+  "useCalendar": false,
+  "displayCalendarIOBandCOB": false,
+  "displayCalendarEmojis": false
 }
 }

+ 4 - 4
FreeAPS/Resources/json/defaults/settings/bg_targets.json

@@ -1,10 +1,10 @@
 {
 {
-    "units": "mmol/L",
-    "user_preferred_units": "mmol/L",
+    "units": "mg/dL",
+    "user_preferred_units": "mg/dL",
     "targets": [
     "targets": [
         {
         {
-            "low": 5.5,
-            "high": 5.5,
+            "low": 100,
+            "high": 100,
             "start": "00:00:00",
             "start": "00:00:00",
             "offset": 0
             "offset": 0
         }
         }

+ 3 - 3
FreeAPS/Resources/json/defaults/settings/insulin_sensitivities.json

@@ -1,9 +1,9 @@
 {
 {
-    "units": "mmol/L",
-    "user_preferred_units": "mmol/L",
+    "units": "mg/dL",
+    "user_preferred_units": "mg/dL",
     "sensitivities": [
     "sensitivities": [
         {
         {
-            "sensitivity": 3.0,
+            "sensitivity": 54,
             "offset": 0,
             "offset": 0,
             "start": "00:00:00"
             "start": "00:00:00"
         }
         }

+ 16 - 6
FreeAPS/Sources/APS/APSManager.swift

@@ -131,6 +131,15 @@ final class BaseAPSManager: APSManager, Injectable {
     }
     }
 
 
     private func subscribe() {
     private func subscribe() {
+        if settingsManager.settings.units == .mmolL {
+            let wasParsed = storage.parseOnFileSettingsToMgdL()
+            if wasParsed {
+                Task {
+                    try await makeProfiles()
+                }
+            }
+        }
+
         deviceDataManager.recommendsLoop
         deviceDataManager.recommendsLoop
             .receive(on: processQueue)
             .receive(on: processQueue)
             .sink { [weak self] in
             .sink { [weak self] in
@@ -930,11 +939,13 @@ final class BaseAPSManager: APSManager, Injectable {
             batchSize: batchSize
             batchSize: batchSize
         )
         )
 
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return []
-        }
+        return await privateContext.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return []
+            }
 
 
-        return glucoseResults
+            return glucoseResults
+        }
     }
     }
 
 
     // TODO: - Refactor this whole shit here...
     // TODO: - Refactor this whole shit here...
@@ -1018,7 +1029,7 @@ final class BaseAPSManager: APSManager, Injectable {
             }
             }
 
 
             // Insulin placeholder
             // Insulin placeholder
-            var insulin = Ins(
+            let insulin = Ins(
                 TDD: 0,
                 TDD: 0,
                 bolus: 0,
                 bolus: 0,
                 temp_basal: 0,
                 temp_basal: 0,
@@ -1057,7 +1068,6 @@ final class BaseAPSManager: APSManager, Injectable {
                 )
                 )
             )
             )
             storage.save(dailystat, as: file)
             storage.save(dailystat, as: file)
-            await nightscout.uploadStatistics(dailystat: dailystat)
 
 
             await saveStatsToCoreData()
             await saveStatsToCoreData()
         }
         }

+ 1 - 36
FreeAPS/Sources/APS/CGM/CGMType.swift

@@ -5,12 +5,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     case none
     case none
     case nightscout
     case nightscout
     case xdrip
     case xdrip
-//    case dexcomG5
-//    case dexcomG6
-//    case dexcomG7
     case simulator
     case simulator
-//    case libreTransmitter
-    case glucoseDirect
     case enlite
     case enlite
     case plugin
     case plugin
 
 
@@ -22,18 +17,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return "Nightscout"
             return "Nightscout"
         case .xdrip:
         case .xdrip:
             return "xDrip4iOS"
             return "xDrip4iOS"
-        case .glucoseDirect:
-            return "Glucose Direct"
-//        case .dexcomG5:
-//            return "Dexcom G5"
-//        case .dexcomG6:
-//            return "Dexcom G6"
-//        case .dexcomG7:
-//            return "Dexcom G7"
         case .simulator:
         case .simulator:
             return NSLocalizedString("Glucose Simulator", comment: "Glucose Simulator CGM type")
             return NSLocalizedString("Glucose Simulator", comment: "Glucose Simulator CGM type")
-//        case .libreTransmitter:
-//            return NSLocalizedString("Libre Transmitter", comment: "Libre Transmitter type")
         case .enlite:
         case .enlite:
             return "Medtronic Enlite"
             return "Medtronic Enlite"
         case .plugin:
         case .plugin:
@@ -49,8 +34,6 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
             return nil
             return nil
         case .xdrip:
         case .xdrip:
             return URL(string: "xdripswift://")!
             return URL(string: "xdripswift://")!
-        case .glucoseDirect:
-            return URL(string: "libredirect://")!
         case .simulator:
         case .simulator:
             return nil
             return nil
         case .plugin:
         case .plugin:
@@ -61,9 +44,7 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
     var externalLink: URL? {
     var externalLink: URL? {
         switch self {
         switch self {
         case .xdrip:
         case .xdrip:
-            return URL(string: "https://github.com/JohanDegraeve/xdripswift")!
-        case .glucoseDirect:
-            return URL(string: "https://github.com/creepymonster/GlucoseDirectApp")!
+            return URL(string: "https://xdrip4ios.readthedocs.io/")!
         default: return nil
         default: return nil
         }
         }
     }
     }
@@ -79,24 +60,8 @@ enum CGMType: String, JSON, CaseIterable, Identifiable {
                 "Using shared app group with external CGM app xDrip4iOS",
                 "Using shared app group with external CGM app xDrip4iOS",
                 comment: "Shared app group xDrip4iOS"
                 comment: "Shared app group xDrip4iOS"
             )
             )
-//        case .dexcomG5:
-//            return NSLocalizedString("Native G5 app", comment: "Native G5 app")
-//        case .dexcomG6:
-//            return NSLocalizedString("Dexcom G6 app", comment: "Dexcom G6 app")
-//        case .dexcomG7:
-//            return NSLocalizedString("Dexcom G7 app", comment: "Dexcom G76 app")
         case .simulator:
         case .simulator:
             return NSLocalizedString("Simple simulator", comment: "Simple simulator")
             return NSLocalizedString("Simple simulator", comment: "Simple simulator")
-//        case .libreTransmitter:
-//            return NSLocalizedString(
-//                "Direct connection with Libre 1 transmitters or European Libre 2 sensors",
-//                comment: "Direct connection with Libre 1 transmitters or European Libre 2 sensors"
-//            )
-        case .glucoseDirect:
-            return NSLocalizedString(
-                "Using shared app group with external CGM app GlucoseDirect",
-                comment: "Shared app group GlucoseDirect"
-            )
         case .enlite:
         case .enlite:
             return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
             return NSLocalizedString("Minilink transmitter", comment: "Minilink transmitter")
         case .plugin:
         case .plugin:

+ 37 - 0
FreeAPS/Sources/APS/DeviceDataManager.swift

@@ -1,5 +1,6 @@
 import Algorithms
 import Algorithms
 import Combine
 import Combine
+import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
@@ -81,6 +82,18 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpDisplayState.value = PumpDisplayState(name: pumpManager.localizedTitle, image: pumpManager.smallImage)
                 pumpName.send(pumpManager.localizedTitle)
                 pumpName.send(pumpManager.localizedTitle)
 
 
+                var modifiedPreferences = settingsManager.preferences
+                let bolusIncrement = Decimal(
+                    pumpManager.supportedBolusVolumes.first ??
+                        Double(
+                            settingsManager.preferences
+                                .bolusIncrement
+                        )
+                )
+                modifiedPreferences
+                    .bolusIncrement = bolusIncrement != 0.025 ? bolusIncrement : 0.1
+                storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
+
                 if let omnipod = pumpManager as? OmnipodPumpManager {
                 if let omnipod = pumpManager as? OmnipodPumpManager {
                     guard let endTime = omnipod.state.podState?.expiresAt else {
                     guard let endTime = omnipod.state.podState?.expiresAt else {
                         pumpExpiresAtDate.send(nil)
                         pumpExpiresAtDate.send(nil)
@@ -140,6 +153,30 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                 pumpDisplayState.value = nil
                 pumpDisplayState.value = nil
                 pumpExpiresAtDate.send(nil)
                 pumpExpiresAtDate.send(nil)
                 pumpName.send("")
                 pumpName.send("")
+                // Reset bolusIncrement setting to default value, which is 0.1 U
+                var modifiedPreferences = settingsManager.preferences
+                modifiedPreferences.bolusIncrement = 0.1
+                storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
+                // Remove OpenAPS_Battery entries
+                Task {
+                    await self.privateContext.perform {
+                        let fetchRequest: NSFetchRequest<OpenAPS_Battery> = OpenAPS_Battery.fetchRequest()
+
+                        do {
+                            let batteryEntries = try self.privateContext.fetch(fetchRequest)
+
+                            for entry in batteryEntries {
+                                self.privateContext.delete(entry)
+                            }
+
+                            guard self.privateContext.hasChanges else { return }
+                            try self.privateContext.save()
+
+                        } catch {
+                            print("Failed to delete OpenAPS_Battery entries: \(error.localizedDescription)")
+                        }
+                    }
+                }
             }
             }
         }
         }
     }
     }

+ 0 - 2
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -136,8 +136,6 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 glucoseSource = nightscoutManager
                 glucoseSource = nightscoutManager
             case .simulator:
             case .simulator:
                 glucoseSource = simulatorSource
                 glucoseSource = simulatorSource
-            case .glucoseDirect:
-                glucoseSource = AppGroupSource(from: "GlucoseDirect", cgmType: .glucoseDirect)
             case .enlite:
             case .enlite:
                 glucoseSource = deviceDataManager
                 glucoseSource = deviceDataManager
             case .plugin:
             case .plugin:

+ 1 - 1
FreeAPS/Sources/APS/FetchTreatmentsManager.swift

@@ -33,7 +33,7 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
 
 
                     let filteredCarbs = await carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) }
                     let filteredCarbs = await carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) }
                     if filteredCarbs.isNotEmpty {
                     if filteredCarbs.isNotEmpty {
-                        await self.carbsStorage.storeCarbs(filteredCarbs)
+                        await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
                     }
                     }
 
 
                     let filteredTargets = await tempTargets.filter { !($0.enteredBy?.contains(TempTarget.manual) ?? false) }
                     let filteredTargets = await tempTargets.filter { !($0.enteredBy?.contains(TempTarget.manual) ?? false) }

+ 26 - 25
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -102,17 +102,19 @@ final class OpenAPS {
             fetchLimit: 2
             fetchLimit: 2
         )
         )
 
 
-        guard let previousDeterminations = results as? [OrefDetermination] else {
-            return
-        }
+        await context.perform {
+            guard let previousDeterminations = results as? [OrefDetermination] else {
+                return
+            }
 
 
-        // We need to get the second last Determination for this comparison because we have saved the current Determination already to Core Data
-        if let previousDetermination = previousDeterminations.dropFirst().first {
-            let iobChanged = previousDetermination.iob != decimalToNSDecimalNumber(determination.iob)
-            let cobChanged = previousDetermination.cob != Int16(Int(determination.cob ?? 0))
+            // We need to get the second last Determination for this comparison because we have saved the current Determination already to Core Data
+            if let previousDetermination = previousDeterminations.dropFirst().first {
+                let iobChanged = previousDetermination.iob != self.decimalToNSDecimalNumber(determination.iob)
+                let cobChanged = previousDetermination.cob != Int16(Int(determination.cob ?? 0))
 
 
-            if iobChanged || cobChanged {
-                Foundation.NotificationCenter.default.post(name: .didUpdateCobIob, object: nil)
+                if iobChanged || cobChanged {
+                    Foundation.NotificationCenter.default.post(name: .didUpdateCobIob, object: nil)
+                }
             }
             }
         }
         }
     }
     }
@@ -140,11 +142,11 @@ final class OpenAPS {
             batchSize: 24
             batchSize: 24
         )
         )
 
 
-        guard let glucoseResults = results as? [GlucoseStored] else {
-            return ""
-        }
-
         return await context.perform {
         return await context.perform {
+            guard let glucoseResults = results as? [GlucoseStored] else {
+                return ""
+            }
+
             // convert to JSON
             // convert to JSON
             return self.jsonConverter.convertToJSON(glucoseResults)
             return self.jsonConverter.convertToJSON(glucoseResults)
         }
         }
@@ -159,11 +161,11 @@ final class OpenAPS {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbResults = results as? [CarbEntryStored] else {
-            return ""
-        }
-
         let json = await context.perform {
         let json = await context.perform {
+            guard let carbResults = results as? [CarbEntryStored] else {
+                return ""
+            }
+
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
             var jsonArray = self.jsonConverter.convertToJSON(carbResults)
 
 
             if let additionalCarbs = additionalCarbs {
             if let additionalCarbs = additionalCarbs {
@@ -209,11 +211,11 @@ final class OpenAPS {
             batchSize: 50
             batchSize: 50
         )
         )
 
 
-        guard let pumpEventResults = results as? [PumpEventStored] else {
-            return nil
-        }
-
         return await context.perform {
         return await context.perform {
+            guard let pumpEventResults = results as? [PumpEventStored] else {
+                return nil
+            }
+
             return pumpEventResults.map(\.objectID)
             return pumpEventResults.map(\.objectID)
         }
         }
     }
     }
@@ -249,12 +251,12 @@ final class OpenAPS {
             if let bolusDTO = event.toBolusDTOEnum() {
             if let bolusDTO = event.toBolusDTOEnum() {
                 eventDTOs.append(bolusDTO)
                 eventDTOs.append(bolusDTO)
             }
             }
-            if let tempBasalDTO = event.toTempBasalDTOEnum() {
-                eventDTOs.append(tempBasalDTO)
-            }
             if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
             if let tempBasalDurationDTO = event.toTempBasalDurationDTOEnum() {
                 eventDTOs.append(tempBasalDurationDTO)
                 eventDTOs.append(tempBasalDurationDTO)
             }
             }
+            if let tempBasalDTO = event.toTempBasalDTOEnum() {
+                eventDTOs.append(tempBasalDTO)
+            }
             return eventDTOs
             return eventDTOs
         }
         }
         return dtos
         return dtos
@@ -466,7 +468,6 @@ final class OpenAPS {
             let weighted_average = weight * average2hours + (1 - weight) * average14
             let weighted_average = weight * average2hours + (1 - weight) * average14
 
 
             var duration: Decimal = 0
             var duration: Decimal = 0
-            var newDuration: Decimal = 0
             var overrideTarget: Decimal = 0
             var overrideTarget: Decimal = 0
 
 
             if useOverride {
             if useOverride {

+ 2 - 2
FreeAPS/Sources/APS/PluginManager.swift

@@ -26,10 +26,10 @@ class BasePluginManager: Injectable, PluginManager {
                 {
                 {
                     if let bundle = Bundle(url: pluginURL) {
                     if let bundle = Bundle(url: pluginURL) {
                         if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
                         if let bname = bundle.object(forInfoDictionaryKey: "CFBundleName") as? String {
-                            debug(.deviceManager, "bundle name2:\(bname)")
+                            debug(.deviceManager, "bundle name: \(bname)")
                         }
                         }
                         if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
                         if let bcgm = bundle.object(forInfoDictionaryKey: "com.loopkit.Loop.CGMManagerIdentifier") as? String {
-                            debug(.deviceManager, "bundle is CGM")
+                            debug(.deviceManager, "bundle is CGM: \(bcgm)")
                         }
                         }
 
 
                         if bundle.isLoopPlugin {
                         if bundle.isLoopPlugin {

+ 108 - 18
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import SwiftDate
 import SwiftDate
@@ -8,7 +9,9 @@ protocol CarbsObserver {
 }
 }
 
 
 protocol CarbsStorage {
 protocol CarbsStorage {
-    func storeCarbs(_ carbs: [CarbsEntry]) async
+    var updatePublisher: AnyPublisher<Void, Never> { get }
+    func storeCarbs(_ carbs: [CarbsEntry], areFetchedFromRemote: Bool) async
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func recent() -> [CarbsEntry]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func getCarbsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
@@ -24,13 +27,53 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
 
 
-    func storeCarbs(_ entries: [CarbsEntry]) async {
-        await saveCarbEquivalents(entries: entries)
-        await saveCarbsToCoreData(entries: entries)
+    func storeCarbs(_ entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
+        var entriesToStore = entries
+
+        if areFetchedFromRemote {
+            entriesToStore = await filterRemoteEntries(entries: entriesToStore)
+        }
+
+        await saveCarbEquivalents(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+        await saveCarbsToCoreData(entries: entriesToStore, areFetchedFromRemote: areFetchedFromRemote)
+    }
+
+    private func filterRemoteEntries(entries: [CarbsEntry]) async -> [CarbsEntry] {
+        // Fetch only the date property from Core Data
+        guard let existing24hCarbEntries = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: CarbEntryStored.self,
+            onContext: coredataContext,
+            predicate: NSPredicate.predicateForOneDayAgo,
+            key: "date",
+            ascending: false,
+            batchSize: 50,
+            propertiesToFetch: ["date", "objectID"]
+        ) as? [[String: Any]] else {
+            return entries
+        }
+
+        // Extract dates into a set for efficient lookup
+        // Since we are not dealing with NSManagedObjects directly it is safe to pass properties between threads
+        let existingTimestamps = Set(existing24hCarbEntries.compactMap { $0["date"] as? Date })
+
+        // Remove all entries that have a matching date in existingTimestamps
+        var filteredEntries = entries
+        filteredEntries.removeAll { entry in
+            let entryDate = entry.actualDate ?? entry.createdAt
+            return existingTimestamps.contains(entryDate)
+        }
+
+        return filteredEntries
     }
     }
 
 
     /**
     /**
@@ -129,7 +172,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         return (futureCarbArray, carbEquivalents)
         return (futureCarbArray, carbEquivalents)
     }
     }
 
 
-    private func saveCarbEquivalents(entries: [CarbsEntry]) async {
+    private func saveCarbEquivalents(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
         guard let lastEntry = entries.last else { return }
         guard let lastEntry = entries.last else { return }
 
 
         if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
         if let fat = lastEntry.fat, let protein = lastEntry.protein, fat > 0 || protein > 0 {
@@ -142,12 +185,12 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             )
             )
 
 
             if carbEquivalentCount > 0 {
             if carbEquivalentCount > 0 {
-                await saveFPUToCoreDataAsBatchInsert(entries: futureCarbEquivalents)
+                await saveFPUToCoreDataAsBatchInsert(entries: futureCarbEquivalents, areFetchedFromRemote: areFetchedFromRemote)
             }
             }
         }
         }
     }
     }
 
 
-    private func saveCarbsToCoreData(entries: [CarbsEntry]) async {
+    private func saveCarbsToCoreData(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
         guard let entry = entries.last, entry.carbs != 0 else { return }
         guard let entry = entries.last, entry.carbs != 0 else { return }
 
 
         await coredataContext.perform {
         await coredataContext.perform {
@@ -159,7 +202,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             newItem.note = entry.note
             newItem.note = entry.note
             newItem.id = UUID()
             newItem.id = UUID()
             newItem.isFPU = false
             newItem.isFPU = false
-            newItem.isUploadedToNS = false
+            newItem.isUploadedToNS = areFetchedFromRemote ? true : false
 
 
             do {
             do {
                 guard self.coredataContext.hasChanges else { return }
                 guard self.coredataContext.hasChanges else { return }
@@ -170,7 +213,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         }
         }
     }
     }
 
 
-    private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry]) async {
+    private func saveFPUToCoreDataAsBatchInsert(entries: [CarbsEntry], areFetchedFromRemote: Bool) async {
         let commonFPUID =
         let commonFPUID =
             UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
             UUID() // all fpus should only get ONE id per batch insert to be able to delete them referencing the fpuID
         var entrySlice = ArraySlice(entries) // convert to ArraySlice
         var entrySlice = ArraySlice(entries) // convert to ArraySlice
@@ -185,7 +228,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             carbEntry.id = UUID.init(uuidString: entryId)
             carbEntry.id = UUID.init(uuidString: entryId)
             carbEntry.fpuID = commonFPUID
             carbEntry.fpuID = commonFPUID
             carbEntry.isFPU = true
             carbEntry.isFPU = true
-            carbEntry.isUploadedToNS = false
+            carbEntry.isUploadedToNS = areFetchedFromRemote ? true : false
             return false // return false to continue
             return false // return false to continue
         }
         }
         await coredataContext.perform {
         await coredataContext.perform {
@@ -193,8 +236,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 try self.coredataContext.execute(batchInsert)
                 try self.coredataContext.execute(batchInsert)
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.succeeded) saved fpus to core data")
 
 
-                // Send notification for triggering a fetch in Home State Model to update the FPU Array
-                Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
+                // Notify subscriber in Home State Model to update the FPU Array
+                self.updateSubject.send(())
             } catch {
             } catch {
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
                 debugPrint("Carbs Storage: \(DebuggingIdentifiers.failed) error while saving fpus to core data")
             }
             }
@@ -209,6 +252,53 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
     }
 
 
+    func deleteCarbs(_ treatmentObjectID: NSManagedObjectID) async {
+        let taskContext = CoreDataStack.shared.newTaskContext()
+        taskContext.name = "deleteContext"
+        taskContext.transactionAuthor = "deleteCarbs"
+
+        var carbEntry: CarbEntryStored?
+
+        await taskContext.perform {
+            do {
+                carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
+                guard let carbEntry = carbEntry else {
+                    debugPrint("Carb entry for batch delete not found. \(DebuggingIdentifiers.failed)")
+                    return
+                }
+
+                if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
+                    // fetch request for all carb entries with the same id
+                    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
+                    fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
+
+                    // NSBatchDeleteRequest
+                    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
+                    deleteRequest.resultType = .resultTypeCount
+
+                    // execute the batch delete request
+                    let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
+                    debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
+
+                    // Notifiy subscribers of the batch delete
+                    self.updateSubject.send(())
+                } else {
+                    taskContext.delete(carbEntry)
+
+                    guard taskContext.hasChanges else { return }
+                    try taskContext.save()
+
+                    debugPrint(
+                        "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
+                    )
+                }
+
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+            }
+        }
+    }
+
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
     func deleteCarbs(at uniqueID: String, fpuID: String, complex: Bool) {
         processQueue.sync {
         processQueue.sync {
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
@@ -248,11 +338,11 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let carbEntries = results as? [CarbEntryStored] else {
-            return []
-        }
-
         return await coredataContext.perform {
         return await coredataContext.perform {
+            guard let carbEntries = results as? [CarbEntryStored] else {
+                return []
+            }
+
             return carbEntries.map { result in
             return carbEntries.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
                     duration: nil,
                     duration: nil,
@@ -287,9 +377,9 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
-
         return await coredataContext.perform {
         return await coredataContext.perform {
+            guard let fpuEntries = results as? [CarbEntryStored] else { return [] }
+
             return fpuEntries.map { result in
             return fpuEntries.map { result in
                 NightscoutTreatment(
                 NightscoutTreatment(
                     duration: nil,
                     duration: nil,

+ 38 - 6
FreeAPS/Sources/APS/Storage/DeterminationStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import Swinject
 import Swinject
@@ -6,6 +7,10 @@ protocol DeterminationStorage {
     func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
     func fetchLastDeterminationObjectID(predicate: NSPredicate) async -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastIDs(for determinationID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
     func getForecastValueIDs(for forecastID: NSManagedObjectID, in context: NSManagedObjectContext) async -> [NSManagedObjectID]
+    func fetchForecastObjects(
+        for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
+        in context: NSManagedObjectContext
+    ) async -> (UUID, Forecast?, [ForecastValue])
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
     func getOrefDeterminationNotYetUploadedToNightscout(_ determinationIds: [NSManagedObjectID]) async -> Determination?
 }
 }
 
 
@@ -27,10 +32,10 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
-            fetchedResults.map(\.objectID)
+            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
+
+            return fetchedResults.map(\.objectID)
         }
         }
     }
     }
 
 
@@ -38,7 +43,7 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         await context.perform {
         await context.perform {
             do {
             do {
                 guard let determination = try context.existingObject(with: determinationID) as? OrefDetermination,
                 guard let determination = try context.existingObject(with: determinationID) as? OrefDetermination,
-                      let forecastSet = determination.forecasts as? Set<NSManagedObject>
+                      let forecastSet = determination.forecasts
                 else {
                 else {
                     return []
                     return []
                 }
                 }
@@ -72,6 +77,35 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
         }
         }
     }
     }
 
 
+    // Fetch forecast value IDs for a given data set
+    func fetchForecastObjects(
+        for data: (id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID]),
+        in context: NSManagedObjectContext
+    ) async -> (UUID, Forecast?, [ForecastValue]) {
+        var forecast: Forecast?
+        var forecastValues: [ForecastValue] = []
+
+        do {
+            try await context.perform {
+                // Fetch the forecast object
+                forecast = try context.existingObject(with: data.forecastID) as? Forecast
+
+                // Fetch the first 3h of forecast values
+                for forecastValueID in data.forecastValueIDs.prefix(36) {
+                    if let forecastValue = try context.existingObject(with: forecastValueID) as? ForecastValue {
+                        forecastValues.append(forecastValue)
+                    }
+                }
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch forecast Values with error: \(error.localizedDescription)"
+            )
+        }
+
+        return (data.id, forecast, forecastValues)
+    }
+
     // Convert NSDecimalNumber to Decimal
     // Convert NSDecimalNumber to Decimal
     func decimal(from nsDecimalNumber: NSDecimalNumber?) -> Decimal {
     func decimal(from nsDecimalNumber: NSDecimalNumber?) -> Decimal {
         nsDecimalNumber?.decimalValue ?? 0.0
         nsDecimalNumber?.decimalValue ?? 0.0
@@ -126,8 +160,6 @@ final class BaseDeterminationStorage: DeterminationStorage, Injectable {
 
 
                 // Check if the fetched object is of the expected type
                 // Check if the fetched object is of the expected type
                 if let orefDetermination = orefDetermination {
                 if let orefDetermination = orefDetermination {
-                    let forecastSet = orefDetermination.forecasts
-
                     result = Determination(
                     result = Determination(
                         id: orefDetermination.id ?? UUID(),
                         id: orefDetermination.id ?? UUID(),
                         reason: orefDetermination.reason ?? "",
                         reason: orefDetermination.reason ?? "",

+ 13 - 7
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -1,4 +1,5 @@
 import AVFAudio
 import AVFAudio
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import SwiftDate
 import SwiftDate
@@ -6,6 +7,7 @@ import SwiftUI
 import Swinject
 import Swinject
 
 
 protocol GlucoseStorage {
 protocol GlucoseStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeGlucose(_ glucose: [BloodGlucose])
     func storeGlucose(_ glucose: [BloodGlucose])
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func syncDate() -> Date
@@ -26,6 +28,12 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
 
     let coredataContext = CoreDataStack.shared.newTaskContext()
     let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     private enum Config {
     private enum Config {
         static let filterTime: TimeInterval = 3.5 * 60
         static let filterTime: TimeInterval = 3.5 * 60
     }
     }
@@ -87,12 +95,10 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                 // process batch insert
                 // process batch insert
                 do {
                 do {
                     try self.coredataContext.execute(batchInsert)
                     try self.coredataContext.execute(batchInsert)
-//                    debugPrint("Glucose Storage: \(#function) \(DebuggingIdentifiers.succeeded) saved glucose to Core Data")
 
 
-                    // Send notification for triggering a fetch in Home State Model to update the Glucose Array
-                    /// This is necessary because changes only get merged automatically into the viewContext because of the Persistent History Tracking
-                    /// But I do not want to fetch on the Main Thread using the @FetchRequest property, I also can not use the FetchedResultsController because of the architecture of the State Model (it must inherit from BaseStateModel and therefore can not inherit from NSObject as well) and because of the fact that I am using a batch insert here there are no notifications sent from the managedObjectContext because changes are directly stored in the persistent container
-                    Foundation.NotificationCenter.default.post(name: .didPerformBatchInsert, object: nil)
+                    // Notify subscribers that there is a new glucose value
+                    // We need to do this because the due to the batch insert there is no ManagedObjectContext notification
+                    self.updateSubject.send(())
                 } catch {
                 } catch {
                     debugPrint(
                     debugPrint(
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
                         "Glucose Storage: \(#function) \(DebuggingIdentifiers.failed) failed to execute batch insert: \(error)"
@@ -246,9 +252,9 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
-
         return await coredataContext.perform {
         return await coredataContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+
             return fetchedResults.map { result in
             return fetchedResults.map { result in
                 BloodGlucose(
                 BloodGlucose(
                     _id: result.id?.uuidString ?? UUID().uuidString,
                     _id: result.id?.uuidString ?? UUID().uuidString,

+ 10 - 10
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -45,9 +45,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             fetchLimit: 1
             fetchLimit: 1
         )
         )
 
 
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -62,9 +62,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             fetchLimit: fetchLimit
             fetchLimit: fetchLimit
         )
         )
 
 
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -79,9 +79,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: true
             ascending: true
         )
         )
 
 
-        guard let fetchedResults = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedResults = results as? [OverrideStored] else { return [] }
+
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -220,9 +220,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedOverrides = results as? [OverrideStored] else { return [] }
+
             return fetchedOverrides.map { override in
             return fetchedOverrides.map { override in
                 let duration = override.indefinite ? 1440 : override.duration ?? 0 // 1440 min = 1 day
                 let duration = override.indefinite ? 1440 : override.duration ?? 0 // 1440 min = 1 day
                 return NightscoutExercise(
                 return NightscoutExercise(
@@ -250,9 +250,9 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             ascending: false
             ascending: false
         )
         )
 
 
-        guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
-
         return await backgroundContext.perform {
         return await backgroundContext.perform {
+            guard let fetchedOverrideRuns = results as? [OverrideRunStored] else { return [] }
+
             return fetchedOverrideRuns.map { overrideRun in
             return fetchedOverrideRuns.map { overrideRun in
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
                 var durationInMinutes = (overrideRun.endDate?.timeIntervalSince(overrideRun.startDate ?? Date()) ?? 1) / 60
                 durationInMinutes = durationInMinutes < 1 ? 1 : durationInMinutes
                 durationInMinutes = durationInMinutes < 1 ? 1 : durationInMinutes

+ 15 - 3
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -9,6 +10,7 @@ protocol PumpHistoryObserver {
 }
 }
 
 
 protocol PumpHistoryStorage {
 protocol PumpHistoryStorage {
+    var updatePublisher: AnyPublisher<Void, Never> { get }
     func storePumpEvents(_ events: [NewPumpEvent])
     func storePumpEvents(_ events: [NewPumpEvent])
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func storeExternalInsulinEvent(amount: Decimal, timestamp: Date) async
     func recent() -> [PumpHistoryEvent]
     func recent() -> [PumpHistoryEvent]
@@ -22,6 +24,12 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var settings: SettingsManager!
     @Injected() private var settings: SettingsManager!
 
 
+    private let updateSubject = PassthroughSubject<Void, Never>()
+
+    var updatePublisher: AnyPublisher<Void, Never> {
+        updateSubject.eraseToAnyPublisher()
+    }
+
     init(resolver: Resolver) {
     init(resolver: Resolver) {
         injectServices(resolver)
         injectServices(resolver)
     }
     }
@@ -190,6 +198,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                 do {
                 do {
                     guard self.context.hasChanges else { return }
                     guard self.context.hasChanges else { return }
                     try self.context.save()
                     try self.context.save()
+
+                    self.updateSubject.send(())
                     debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
                     debugPrint("\(DebuggingIdentifiers.succeeded) stored pump events in Core Data")
                 } catch let error as NSError {
                 } catch let error as NSError {
                     debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
                     debugPrint("\(DebuggingIdentifiers.failed) failed to store pump events with error: \(error.userInfo)")
@@ -218,6 +228,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             do {
             do {
                 guard self.context.hasChanges else { return }
                 guard self.context.hasChanges else { return }
                 try self.context.save()
                 try self.context.save()
+
+                self.updateSubject.send(())
             } catch {
             } catch {
                 print(error.localizedDescription)
                 print(error.localizedDescription)
             }
             }
@@ -262,10 +274,10 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
-
         return await context.perform { [self] in
         return await context.perform { [self] in
-            fetchedPumpEvents.map { event in
+            guard let fetchedPumpEvents = results as? [PumpEventStored] else { return [] }
+
+            return fetchedPumpEvents.map { event in
                 switch event.type {
                 switch event.type {
                 case PumpEvent.bolus.rawValue:
                 case PumpEvent.bolus.rawValue:
                     // eventType determines whether bolus is external, smb or manual (=administered via app by user)
                     // eventType determines whether bolus is external, smb or manual (=administered via app by user)

+ 15 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -10,6 +10,9 @@ import Swinject
 
 
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
     @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
 
 
+    // Read the color scheme preference from UserDefaults; defaults to system default setting
+    @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
+
     let coreDataStack = CoreDataStack.shared
     let coreDataStack = CoreDataStack.shared
 
 
     // Dependencies Assembler
     // Dependencies Assembler
@@ -67,6 +70,7 @@ import Swinject
     var body: some Scene {
     var body: some Scene {
         WindowGroup {
         WindowGroup {
             Main.RootView(resolver: resolver)
             Main.RootView(resolver: resolver)
+                .preferredColorScheme(colorScheme(for: colorSchemePreference ?? .systemDefault) ?? nil)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
                 .environmentObject(Icons())
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)
                 .onOpenURL(perform: handleURL)
@@ -85,6 +89,17 @@ import Swinject
         }
         }
     }
     }
 
 
+    private func colorScheme(for colorScheme: ColorSchemeOption) -> ColorScheme? {
+        switch colorScheme {
+        case .systemDefault:
+            return nil // Uses the system theme.
+        case .light:
+            return .light
+        case .dark:
+            return .dark
+        }
+    }
+
     func scheduleDatabaseCleaning() {
     func scheduleDatabaseCleaning() {
         let request = BGAppRefreshTaskRequest(identifier: "com.openiaps.cleanup")
         let request = BGAppRefreshTaskRequest(identifier: "com.openiaps.cleanup")
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days
         request.earliestBeginDate = .now.addingTimeInterval(7 * 24 * 60 * 60) // 7 days

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

@@ -1,4 +1,3 @@
-import CoreData
 import Foundation
 import Foundation
 import SwiftUI
 import SwiftUI
 
 
@@ -8,7 +7,7 @@ public func getDynamicGlucoseColor(
     highGlucoseColorValue: Decimal,
     highGlucoseColorValue: Decimal,
     lowGlucoseColorValue: Decimal,
     lowGlucoseColorValue: Decimal,
     targetGlucose: Decimal,
     targetGlucose: Decimal,
-    dynamicGlucoseColor: Bool,
+    glucoseColorStyle: GlucoseColorStyle,
     offset: Decimal
     offset: Decimal
 ) -> Color {
 ) -> Color {
     // Convert Decimal to Int for high and low glucose values
     // Convert Decimal to Int for high and low glucose values
@@ -17,7 +16,7 @@ public func getDynamicGlucoseColor(
     let targetGlucose = targetGlucose
     let targetGlucose = targetGlucose
 
 
     // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
     // Only use calculateHueBasedGlucoseColor if the setting is enabled in preferences
-    if dynamicGlucoseColor {
+    if GlucoseColorStyle == .dynamicColor {
         return calculateHueBasedGlucoseColor(
         return calculateHueBasedGlucoseColor(
             glucoseValue: glucoseValue,
             glucoseValue: glucoseValue,
             highGlucose: highGlucose,
             highGlucose: highGlucose,

+ 99 - 0
FreeAPS/Sources/Helpers/MainChartHelper.swift

@@ -0,0 +1,99 @@
+import CoreData
+import Foundation
+
+enum MainChartHelper {
+    // Calculates the glucose value thats the nearest to parameter 'time'
+    /// -Returns: A NSManagedObject of GlucoseStored
+    /// it is thread safe as everything is executed on the main thread
+    static func timeToNearestGlucose(glucoseValues: [GlucoseStored], time: TimeInterval) -> GlucoseStored? {
+        guard !glucoseValues.isEmpty else {
+            return nil
+        }
+
+        var low = 0
+        var high = glucoseValues.count - 1
+        var closestGlucose: GlucoseStored?
+
+        // binary search to find next glucose
+        while low <= high {
+            let mid = low + (high - low) / 2
+            let midTime = glucoseValues[mid].date?.timeIntervalSince1970 ?? 0
+
+            if midTime == time {
+                return glucoseValues[mid]
+            } else if midTime < time {
+                low = mid + 1
+            } else {
+                high = mid - 1
+            }
+
+            // update if necessary
+            if closestGlucose == nil || abs(midTime - time) < abs(closestGlucose!.date?.timeIntervalSince1970 ?? 0 - time) {
+                closestGlucose = glucoseValues[mid]
+            }
+        }
+
+        return closestGlucose
+    }
+
+    enum Config {
+        static let bolusSize: CGFloat = 5
+        static let bolusScale: CGFloat = 1.8
+        static let carbsSize: CGFloat = 5
+        static let maxCarbSize: CGFloat = 30
+        static let carbsScale: CGFloat = 0.3
+        static let fpuSize: CGFloat = 10
+        static let maxGlucose = 270
+        static let minGlucose = 45
+    }
+
+    static var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumIntegerDigits = 0
+        formatter.maximumFractionDigits = 2
+        formatter.decimalSeparator = "."
+        return formatter
+    }
+
+    static var carbsFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }
+
+    static func bolusOffset(units: GlucoseUnits) -> Decimal {
+        units == .mgdL ? 30 : 1.66
+    }
+
+    static func calculateDuration(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> TimeInterval? {
+        do {
+            if let override = try context.existingObject(with: objectID) as? OverrideStored,
+               let overrideDuration = override.duration as? Double, overrideDuration != 0
+            {
+                return TimeInterval(overrideDuration * 60) // return seconds
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+            )
+        }
+        return nil
+    }
+
+    static func calculateTarget(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> Decimal? {
+        do {
+            if let override = try context.existingObject(with: objectID) as? OverrideStored,
+               let overrideTarget = override.target, overrideTarget != 0
+            {
+                return overrideTarget.decimalValue
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+            )
+        }
+        return nil
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 1
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings


+ 4 - 4
FreeAPS/Sources/Models/BGTargets.swift

@@ -1,15 +1,15 @@
 import Foundation
 import Foundation
 
 
 struct BGTargets: JSON {
 struct BGTargets: JSON {
-    let units: GlucoseUnits
-    let userPrefferedUnits: GlucoseUnits
-    let targets: [BGTargetEntry]
+    var units: GlucoseUnits
+    var userPreferredUnits: GlucoseUnits
+    var targets: [BGTargetEntry]
 }
 }
 
 
 extension BGTargets {
 extension BGTargets {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
         case units
         case units
-        case userPrefferedUnits = "user_preferred_units"
+        case userPreferredUnits = "user_preferred_units"
         case targets
         case targets
     }
     }
 }
 }

+ 0 - 10
FreeAPS/Sources/Models/BloodGlucose.swift

@@ -20,43 +20,33 @@ struct BloodGlucose: JSON, Identifiable, Hashable {
         init?(from string: String) {
         init?(from string: String) {
             switch string {
             switch string {
             case "\u{2191}\u{2191}\u{2191}",
             case "\u{2191}\u{2191}\u{2191}",
-                 "↑↑↑",
                  "TripleUp":
                  "TripleUp":
                 self = .tripleUp
                 self = .tripleUp
             case "\u{2191}\u{2191}",
             case "\u{2191}\u{2191}",
-                 "↑↑",
                  "DoubleUp":
                  "DoubleUp":
                 self = .doubleUp
                 self = .doubleUp
             case "\u{2191}",
             case "\u{2191}",
-                 "↑",
                  "SingleUp":
                  "SingleUp":
                 self = .singleUp
                 self = .singleUp
             case "\u{2197}",
             case "\u{2197}",
-                 "↗︎",
                  "FortyFiveUp":
                  "FortyFiveUp":
                 self = .fortyFiveUp
                 self = .fortyFiveUp
             case "\u{2192}",
             case "\u{2192}",
-                 "→",
                  "Flat":
                  "Flat":
                 self = .flat
                 self = .flat
             case "\u{2198}",
             case "\u{2198}",
-                 "↘︎",
                  "FortyFiveDown":
                  "FortyFiveDown":
                 self = .fortyFiveDown
                 self = .fortyFiveDown
             case "\u{2193}",
             case "\u{2193}",
-                 "↓",
                  "SingleDown":
                  "SingleDown":
                 self = .singleDown
                 self = .singleDown
             case "\u{2193}\u{2193}",
             case "\u{2193}\u{2193}",
-                 "↓↓",
                  "DoubleDown":
                  "DoubleDown":
                 self = .doubleDown
                 self = .doubleDown
             case "\u{2193}\u{2193}\u{2193}",
             case "\u{2193}\u{2193}\u{2193}",
-                 "↓↓↓",
                  "TripleDown":
                  "TripleDown":
                 self = .tripleDown
                 self = .tripleDown
             case "\u{2194}",
             case "\u{2194}",
-                 "↔︎",
                  "NONE":
                  "NONE":
                 self = .none
                 self = .none
             case "NOT COMPUTABLE":
             case "NOT COMPUTABLE":

+ 15 - 0
FreeAPS/Sources/Models/ColorSchemeOption.swift

@@ -0,0 +1,15 @@
+enum ColorSchemeOption: String, JSON, CaseIterable, Identifiable {
+    var id: String { rawValue }
+
+    case systemDefault
+    case light
+    case dark
+
+    var displayName: String {
+        switch self {
+        case .systemDefault: return "System Default"
+        case .light: return "Light"
+        case .dark: return "Dark"
+        }
+    }
+}

+ 158 - 0
FreeAPS/Sources/Models/DecimalPickerSettings.swift

@@ -0,0 +1,158 @@
+import SwiftUI
+
+class PickerSettingsProvider: ObservableObject {
+    static let shared = PickerSettingsProvider()
+
+    var settings = DecimalPickerSettings()
+
+    private init() {} // Private init to enforce singleton pattern
+
+    // Helper function to generate values for the picker
+    func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits) -> [Decimal] {
+        var values: [Decimal] = []
+        var currentValue = setting.min
+
+        while currentValue <= setting.max {
+            values.append(currentValue)
+            currentValue += setting.step
+        }
+
+        // Glucose values are stored as mg/dl values, so Integers.
+        // Filter out odd numbers to avoid duplicate mmol/L values due to rounding.
+        if units == .mmolL, setting.type == PickerSetting.PickerSettingType.glucose {
+            values = values.filter { Int($0) % 2 == 0 }
+        }
+        return values
+    }
+}
+
+struct DecimalPickerSettings {
+    var lowGlucose = PickerSetting(value: 72, step: 1, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
+    var highGlucose = PickerSetting(value: 270, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
+    var carbsRequiredThreshold = PickerSetting(value: 10, step: 1, min: 0, max: 100, type: PickerSetting.PickerSettingType.gramms)
+    var individualAdjustmentFactor = PickerSetting(
+        value: 0.5,
+        step: 0.05,
+        min: 0.1,
+        max: 2,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var high = PickerSetting(value: 180, step: 1, min: 100, max: 500, type: PickerSetting.PickerSettingType.glucose)
+    var low = PickerSetting(value: 70, step: 1, min: 40, max: 100, type: PickerSetting.PickerSettingType.glucose)
+    var maxCarbs = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gramms)
+    var maxFat = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gramms)
+    var maxProtein = PickerSetting(value: 250, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gramms)
+    var overrideFactor = PickerSetting(value: 0.8, step: 0.05, min: 0.5, max: 1.5, type: PickerSetting.PickerSettingType.factor)
+    var fattyMealFactor = PickerSetting(value: 0.7, step: 0.05, min: 0.5, max: 2, type: PickerSetting.PickerSettingType.factor)
+    var sweetMealFactor = PickerSetting(value: 2, step: 0.05, min: 1, max: 3, type: PickerSetting.PickerSettingType.factor)
+    var maxIOB = PickerSetting(value: 0, step: 1, min: 0, max: 20, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxDailySafetyMultiplier = PickerSetting(
+        value: 3,
+        step: 0.1,
+        min: 1,
+        max: 5,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var currentBasalSafetyMultiplier = PickerSetting(
+        value: 4,
+        step: 0.1,
+        min: 1,
+        max: 5,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var autosensMax = PickerSetting(value: 1.2, step: 0.1, min: 0.5, max: 2, type: PickerSetting.PickerSettingType.factor)
+    var autosensMin = PickerSetting(value: 0.7, step: 0.1, min: 0.5, max: 1, type: PickerSetting.PickerSettingType.factor)
+    var smbDeliveryRatio = PickerSetting(value: 0.5, step: 0.05, min: 0.3, max: 0.7, type: PickerSetting.PickerSettingType.factor)
+    var halfBasalExerciseTarget = PickerSetting(
+        value: 160,
+        step: 1,
+        min: 100,
+        max: 200,
+        type: PickerSetting.PickerSettingType.glucose
+    )
+    var maxCOB = PickerSetting(value: 120, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gramms)
+    var min5mCarbimpact = PickerSetting(value: 8, step: 1, min: 0, max: 20, type: PickerSetting.PickerSettingType.gramms)
+    var autotuneISFAdjustmentFraction = PickerSetting(
+        value: 1.0,
+        step: 0.05,
+        min: 0,
+        max: 1,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var remainingCarbsFraction = PickerSetting(
+        value: 1.0,
+        step: 0.05,
+        min: 0.5,
+        max: 1,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var remainingCarbsCap = PickerSetting(value: 90, step: 5, min: 0, max: 200, type: PickerSetting.PickerSettingType.gramms)
+    var maxSMBBasalMinutes = PickerSetting(value: 30, step: 5, min: 30, max: 180, type: PickerSetting.PickerSettingType.minute)
+    var maxUAMSMBBasalMinutes = PickerSetting(value: 30, step: 5, min: 30, max: 180, type: PickerSetting.PickerSettingType.minute)
+    var smbInterval = PickerSetting(value: 3, step: 1, min: 1, max: 10, type: PickerSetting.PickerSettingType.minute)
+    var bolusIncrement = PickerSetting(
+        value: 0.1,
+        step: 0.05,
+        min: 0.05,
+        max: 1,
+        type: PickerSetting.PickerSettingType.insulinUnit
+    )
+    var insulinPeakTime = PickerSetting(value: 75, step: 5, min: 35, max: 120, type: PickerSetting.PickerSettingType.minute)
+    var carbsReqThreshold = PickerSetting(value: 1.0, step: 0.1, min: 0, max: 10, type: PickerSetting.PickerSettingType.gramms)
+    var noisyCGMTargetMultiplier = PickerSetting(
+        value: 1.3,
+        step: 0.05,
+        min: 1,
+        max: 2,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var maxDeltaBGthreshold = PickerSetting(
+        value: 0.2,
+        step: 0.05,
+        min: 0.1,
+        max: 2,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var adjustmentFactor = PickerSetting(value: 0.8, step: 0.1, min: 0.5, max: 1.5, type: PickerSetting.PickerSettingType.factor)
+    var adjustmentFactorSigmoid = PickerSetting(
+        value: 0.5,
+        step: 0.1,
+        min: 0.5,
+        max: 2,
+        type: PickerSetting.PickerSettingType.factor
+    )
+    var weightPercentage = PickerSetting(value: 0.65, step: 0.1, min: 0.1, max: 1, type: PickerSetting.PickerSettingType.factor)
+    var enableSMB_high_bg_target = PickerSetting(
+        value: 110,
+        step: 1,
+        min: 70,
+        max: 200,
+        type: PickerSetting.PickerSettingType.glucose
+    )
+    var threshold_setting = PickerSetting(value: 60, step: 1, min: 60, max: 120, type: PickerSetting.PickerSettingType.glucose)
+    var updateInterval = PickerSetting(value: 20, step: 5, min: 1, max: 60, type: PickerSetting.PickerSettingType.minute)
+    var delay = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
+    var minuteInterval = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.minute)
+    var timeCap = PickerSetting(value: 20, step: 5, min: 5, max: 60, type: PickerSetting.PickerSettingType.hour)
+    var hours = PickerSetting(value: 6, step: 0.5, min: 2, max: 24, type: PickerSetting.PickerSettingType.hour)
+    var dia = PickerSetting(value: 6, step: 0.5, min: 4, max: 10, type: PickerSetting.PickerSettingType.hour)
+    var maxBolus = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
+    var maxBasal = PickerSetting(value: 10, step: 0.5, min: 1, max: 30, type: PickerSetting.PickerSettingType.insulinUnit)
+}
+
+struct PickerSetting {
+    var value: Decimal
+    var step: Decimal
+    var min: Decimal
+    var max: Decimal
+    var type: PickerSettingType
+
+    enum PickerSettingType {
+        case glucose
+        case factor
+        case gramms
+        case insulinUnit
+        case minute
+        case hour
+    }
+}

+ 10 - 10
FreeAPS/Sources/Models/Determination.swift

@@ -2,10 +2,10 @@ import Foundation
 
 
 struct Determination: JSON, Equatable {
 struct Determination: JSON, Equatable {
     let id: UUID?
     let id: UUID?
-    let reason: String
+    var reason: String
     let units: Decimal?
     let units: Decimal?
     let insulinReq: Decimal?
     let insulinReq: Decimal?
-    let eventualBG: Int?
+    var eventualBG: Int?
     let sensitivityRatio: Decimal?
     let sensitivityRatio: Decimal?
     let rate: Decimal?
     let rate: Decimal?
     let duration: Decimal?
     let duration: Decimal?
@@ -15,20 +15,20 @@ struct Determination: JSON, Equatable {
     var deliverAt: Date?
     var deliverAt: Date?
     let carbsReq: Decimal?
     let carbsReq: Decimal?
     let temp: TempType?
     let temp: TempType?
-    let bg: Decimal?
+    var bg: Decimal?
     let reservoir: Decimal?
     let reservoir: Decimal?
-    let isf: Decimal?
+    var isf: Decimal?
     var timestamp: Date?
     var timestamp: Date?
     let tdd: Decimal?
     let tdd: Decimal?
     let insulin: Insulin?
     let insulin: Insulin?
-    let current_target: Decimal?
+    var current_target: Decimal?
     let insulinForManualBolus: Decimal?
     let insulinForManualBolus: Decimal?
     let manualBolusErrorString: Decimal?
     let manualBolusErrorString: Decimal?
-    let minDelta: Decimal?
-    let expectedDelta: Decimal?
-    let minGuardBG: Decimal?
-    let minPredBG: Decimal?
-    let threshold: Decimal?
+    var minDelta: Decimal?
+    var expectedDelta: Decimal?
+    var minGuardBG: Decimal?
+    var minPredBG: Decimal?
+    var threshold: Decimal?
     let carbRatio: Decimal?
     let carbRatio: Decimal?
     let received: Bool?
     let received: Bool?
 }
 }

+ 16 - 0
FreeAPS/Sources/Models/ForecastDisplayType.swift

@@ -0,0 +1,16 @@
+import Foundation
+
+enum ForecastDisplayType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case cone
+    case lines
+    var displayName: String {
+        switch self {
+        case .cone:
+            return NSLocalizedString("Cone", comment: "")
+
+        case .lines:
+            return NSLocalizedString("Lines", comment: "")
+        }
+    }
+}

+ 12 - 32
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -25,8 +25,6 @@ struct FreeAPSSettings: JSON, Equatable {
     var useLocalGlucoseSource: Bool = false
     var useLocalGlucoseSource: Bool = false
     var localGlucosePort: Int = 8080
     var localGlucosePort: Int = 8080
     var debugOptions: Bool = false
     var debugOptions: Bool = false
-    var insulinReqPercentage: Decimal = 70
-    var skipBolusScreenAfterCarbs: Bool = false
     var displayHR: Bool = false
     var displayHR: Bool = false
     var cgm: CGMType = .none
     var cgm: CGMType = .none
     var cgmPluginIdentifier: String = ""
     var cgmPluginIdentifier: String = ""
@@ -41,9 +39,9 @@ struct FreeAPSSettings: JSON, Equatable {
     var lowGlucose: Decimal = 72
     var lowGlucose: Decimal = 72
     var highGlucose: Decimal = 270
     var highGlucose: Decimal = 270
     var carbsRequiredThreshold: Decimal = 10
     var carbsRequiredThreshold: Decimal = 10
-    var animatedBackground: Bool = false
+    var showCarbsRequiredBadge: Bool = true
     var useFPUconversion: Bool = true
     var useFPUconversion: Bool = true
-    var tins: Bool = false
+    var totalInsulinDisplayType: TotalInsulinDisplayType = .totalDailyDose
     var individualAdjustmentFactor: Decimal = 0.5
     var individualAdjustmentFactor: Decimal = 0.5
     var timeCap: Int = 8
     var timeCap: Int = 8
     var minuteInterval: Int = 30
     var minuteInterval: Int = 30
@@ -55,12 +53,12 @@ struct FreeAPSSettings: JSON, Equatable {
     var high: Decimal = 180
     var high: Decimal = 180
     var low: Decimal = 70
     var low: Decimal = 70
     var hours: Int = 6
     var hours: Int = 6
-    var dynamicGlucoseColor: Bool = false
+    var glucoseColorStyle: GlucoseColorStyle = .staticColor
     var xGridLines: Bool = true
     var xGridLines: Bool = true
     var yGridLines: Bool = true
     var yGridLines: Bool = true
     var oneDimensionalGraph: Bool = false
     var oneDimensionalGraph: Bool = false
     var rulerMarks: Bool = true
     var rulerMarks: Bool = true
-    var displayForecastsAsLines: Bool = false
+    var forecastDisplayType: ForecastDisplayType = .cone
     var maxCarbs: Decimal = 250
     var maxCarbs: Decimal = 250
     var maxFat: Decimal = 250
     var maxFat: Decimal = 250
     var maxProtein: Decimal = 250
     var maxProtein: Decimal = 250
@@ -68,14 +66,12 @@ struct FreeAPSSettings: JSON, Equatable {
     var confirmBolusFaster: Bool = false
     var confirmBolusFaster: Bool = false
     var onlyAutotuneBasals: Bool = false
     var onlyAutotuneBasals: Bool = false
     var overrideFactor: Decimal = 0.8
     var overrideFactor: Decimal = 0.8
-    var useCalc: Bool = true
     var fattyMeals: Bool = false
     var fattyMeals: Bool = false
     var fattyMealFactor: Decimal = 0.7
     var fattyMealFactor: Decimal = 0.7
     var sweetMeals: Bool = false
     var sweetMeals: Bool = false
     var sweetMealFactor: Decimal = 2
     var sweetMealFactor: Decimal = 2
     var displayPresets: Bool = true
     var displayPresets: Bool = true
     var useLiveActivity: Bool = false
     var useLiveActivity: Bool = false
-    var historyLayout: HistoryLayout = .twoTabs
     var lockScreenView: LockScreenView = .simple
     var lockScreenView: LockScreenView = .simple
     var bolusShortcut: BolusShortcutLimit = .notAllowed
     var bolusShortcut: BolusShortcutLimit = .notAllowed
 }
 }
@@ -122,14 +118,6 @@ extension FreeAPSSettings: Decodable {
             settings.debugOptions = debugOptions
             settings.debugOptions = debugOptions
         }
         }
 
 
-        if let insulinReqPercentage = try? container.decode(Decimal.self, forKey: .insulinReqPercentage) {
-            settings.insulinReqPercentage = insulinReqPercentage
-        }
-
-        if let skipBolusScreenAfterCarbs = try? container.decode(Bool.self, forKey: .skipBolusScreenAfterCarbs) {
-            settings.skipBolusScreenAfterCarbs = skipBolusScreenAfterCarbs
-        }
-
         if let displayHR = try? container.decode(Bool.self, forKey: .displayHR) {
         if let displayHR = try? container.decode(Bool.self, forKey: .displayHR) {
             settings.displayHR = displayHR
             settings.displayHR = displayHR
             // compatibility if displayOnWatch is not available in json files
             // compatibility if displayOnWatch is not available in json files
@@ -176,18 +164,14 @@ extension FreeAPSSettings: Decodable {
             settings.useFPUconversion = useFPUconversion
             settings.useFPUconversion = useFPUconversion
         }
         }
 
 
-        if let tins = try? container.decode(Bool.self, forKey: .tins) {
-            settings.tins = tins
+        if let totalInsulinDisplayType = try? container.decode(TotalInsulinDisplayType.self, forKey: .totalInsulinDisplayType) {
+            settings.totalInsulinDisplayType = totalInsulinDisplayType
         }
         }
 
 
         if let individualAdjustmentFactor = try? container.decode(Decimal.self, forKey: .individualAdjustmentFactor) {
         if let individualAdjustmentFactor = try? container.decode(Decimal.self, forKey: .individualAdjustmentFactor) {
             settings.individualAdjustmentFactor = individualAdjustmentFactor
             settings.individualAdjustmentFactor = individualAdjustmentFactor
         }
         }
 
 
-        if let useCalc = try? container.decode(Bool.self, forKey: .useCalc) {
-            settings.useCalc = useCalc
-        }
-
         if let fattyMeals = try? container.decode(Bool.self, forKey: .fattyMeals) {
         if let fattyMeals = try? container.decode(Bool.self, forKey: .fattyMeals) {
             settings.fattyMeals = fattyMeals
             settings.fattyMeals = fattyMeals
         }
         }
@@ -247,8 +231,8 @@ extension FreeAPSSettings: Decodable {
             settings.carbsRequiredThreshold = carbsRequiredThreshold
             settings.carbsRequiredThreshold = carbsRequiredThreshold
         }
         }
 
 
-        if let animatedBackground = try? container.decode(Bool.self, forKey: .animatedBackground) {
-            settings.animatedBackground = animatedBackground
+        if let showCarbsRequiredBadge = try? container.decode(Bool.self, forKey: .showCarbsRequiredBadge) {
+            settings.showCarbsRequiredBadge = showCarbsRequiredBadge
         }
         }
 
 
         if let smoothGlucose = try? container.decode(Bool.self, forKey: .smoothGlucose) {
         if let smoothGlucose = try? container.decode(Bool.self, forKey: .smoothGlucose) {
@@ -267,8 +251,8 @@ extension FreeAPSSettings: Decodable {
             settings.hours = hours
             settings.hours = hours
         }
         }
 
 
-        if let dynamicGlucoseColor = try? container.decode(Bool.self, forKey: .dynamicGlucoseColor) {
-            settings.dynamicGlucoseColor = dynamicGlucoseColor
+        if let glucoseColorStyle = try? container.decode(GlucoseColorStyle.self, forKey: .glucoseColorStyle) {
+            settings.glucoseColorStyle = glucoseColorStyle
         }
         }
 
 
         if let xGridLines = try? container.decode(Bool.self, forKey: .xGridLines) {
         if let xGridLines = try? container.decode(Bool.self, forKey: .xGridLines) {
@@ -287,8 +271,8 @@ extension FreeAPSSettings: Decodable {
             settings.rulerMarks = rulerMarks
             settings.rulerMarks = rulerMarks
         }
         }
 
 
-        if let displayForecastsAsLines = try? container.decode(Bool.self, forKey: .displayForecastsAsLines) {
-            settings.displayForecastsAsLines = displayForecastsAsLines
+        if let forecastDisplayType = try? container.decode(ForecastDisplayType.self, forKey: .forecastDisplayType) {
+            settings.forecastDisplayType = forecastDisplayType
         }
         }
 
 
         if let overrideHbA1cUnit = try? container.decode(Bool.self, forKey: .overrideHbA1cUnit) {
         if let overrideHbA1cUnit = try? container.decode(Bool.self, forKey: .overrideHbA1cUnit) {
@@ -327,10 +311,6 @@ extension FreeAPSSettings: Decodable {
             settings.useLiveActivity = useLiveActivity
             settings.useLiveActivity = useLiveActivity
         }
         }
 
 
-        if let historyLayout = try? container.decode(HistoryLayout.self, forKey: .historyLayout) {
-            settings.historyLayout = historyLayout
-        }
-
         if let lockScreenView = try? container.decode(LockScreenView.self, forKey: .lockScreenView) {
         if let lockScreenView = try? container.decode(LockScreenView.self, forKey: .lockScreenView) {
             settings.lockScreenView = lockScreenView
             settings.lockScreenView = lockScreenView
         }
         }

+ 22 - 0
FreeAPS/Sources/Models/GlucoseColorStyle.swift

@@ -0,0 +1,22 @@
+//
+//  GlucoseColorStyle.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 27.09.24.
+//
+import Foundation
+
+enum GlucoseColorStyle: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case staticColor
+    case dynamicColor
+
+    var displayName: String {
+        switch self {
+        case .staticColor:
+            return "Static"
+        case .dynamicColor:
+            return "Dynamic"
+        }
+    }
+}

+ 0 - 16
FreeAPS/Sources/Models/HistoryLayout.swift

@@ -1,16 +0,0 @@
-import Foundation
-
-enum HistoryLayout: String, JSON, CaseIterable, Identifiable, Codable {
-    var id: String { rawValue }
-    case twoTabs
-    case threeTabs
-
-    var displayName: String {
-        switch self {
-        case .twoTabs:
-            return NSLocalizedString("2 Tabs", comment: "")
-        case .threeTabs:
-            return NSLocalizedString("3 Tabs", comment: "")
-        }
-    }
-}

+ 4 - 4
FreeAPS/Sources/Models/InsulinSensitivities.swift

@@ -1,15 +1,15 @@
 import Foundation
 import Foundation
 
 
 struct InsulinSensitivities: JSON {
 struct InsulinSensitivities: JSON {
-    let units: GlucoseUnits
-    let userPrefferedUnits: GlucoseUnits
-    let sensitivities: [InsulinSensitivityEntry]
+    var units: GlucoseUnits
+    var userPreferredUnits: GlucoseUnits
+    var sensitivities: [InsulinSensitivityEntry]
 }
 }
 
 
 extension InsulinSensitivities {
 extension InsulinSensitivities {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
         case units
         case units
-        case userPrefferedUnits = "user_preferred_units"
+        case userPreferredUnits = "user_preferred_units"
         case sensitivities
         case sensitivities
     }
     }
 }
 }

+ 0 - 6
FreeAPS/Sources/Models/NightscoutStatistics.swift

@@ -1,6 +0,0 @@
-import Foundation
-
-struct NightscoutStatistics: JSON {
-    let report = "statistics"
-    let dailystats: Statistics?
-}

+ 1 - 1
FreeAPS/Sources/Models/Preferences.swift

@@ -52,7 +52,7 @@ struct Preferences: JSON {
     var tddAdjBasal: Bool = false
     var tddAdjBasal: Bool = false
     var enableSMB_high_bg: Bool = false
     var enableSMB_high_bg: Bool = false
     var enableSMB_high_bg_target: Decimal = 110
     var enableSMB_high_bg_target: Decimal = 110
-    var threshold_setting: Decimal = 65
+    var threshold_setting: Decimal = 60
     var updateInterval: Decimal = 20
     var updateInterval: Decimal = 20
 }
 }
 
 

+ 1 - 6
FreeAPS/Sources/Models/RawFetchedProfile.swift

@@ -4,15 +4,10 @@ struct FetchedNightscoutProfileStore: JSON {
     let _id: String
     let _id: String
     let defaultProfile: String
     let defaultProfile: String
     let startDate: String
     let startDate: String
-    // TODO: what is this shit used for?
-    // <<<<<<< HEAD
     let mills: Decimal
     let mills: Decimal
     let enteredBy: String
     let enteredBy: String
-//    let store: [String: ScheduledNightscoutProfile]
+    let store: [String: ScheduledNightscoutProfile]
     let created_at: String
     let created_at: String
-    //=======
-    let store: [String: FetchedNightscoutProfile]
-    // >>>>>>> 9672da256c317a314acc76d6e4f6e82cc174d133
 }
 }
 
 
 struct FetchedNightscoutProfile: JSON {
 struct FetchedNightscoutProfile: JSON {

+ 22 - 0
FreeAPS/Sources/Models/TotalInsulinDisplayType.swift

@@ -0,0 +1,22 @@
+//
+//  TotalInsulinDisplayType.swift
+//  FreeAPS
+//
+//  Created by Cengiz Deniz on 25.08.24.
+//
+import Foundation
+
+enum TotalInsulinDisplayType: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
+    var id: String { rawValue }
+    case totalDailyDose
+    case totalInsulinInScope
+
+    var displayName: String {
+        switch self {
+        case .totalDailyDose:
+            return NSLocalizedString("Total Daily Dose", comment: "")
+        case .totalInsulinInScope:
+            return NSLocalizedString("Total Insulin in Scope", comment: "")
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsDataFlow.swift

@@ -0,0 +1,5 @@
+enum AlgorithmAdvancedSettings {
+    enum Config {}
+}
+
+protocol AlgorithmAdvancedSettingsProvider: Provider {}

+ 64 - 0
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsProvider.swift

@@ -0,0 +1,64 @@
+import Combine
+import Foundation
+import HealthKit
+import LoopKit
+import LoopKitUI
+
+extension AlgorithmAdvancedSettings {
+    final class Provider: BaseProvider, AlgorithmAdvancedSettingsProvider {
+        private let processQueue = DispatchQueue(label: "AlgorithmAdvancedSettingsProvider.processQueue")
+        @Injected() private var broadcaster: Broadcaster!
+
+        func settings() -> PumpSettings {
+            storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+                ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+                ?? PumpSettings(insulinActionCurve: 6.0, maxBolus: 10, maxBasal: 2)
+        }
+
+        func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
+            func save(_ settings: PumpSettings) {
+                storage.save(settings, as: OpenAPS.Settings.settings)
+                processQueue.async {
+                    self.broadcaster.notify(PumpSettingsObserver.self, on: self.processQueue) {
+                        $0.pumpSettingsDidChange(settings)
+                    }
+                }
+            }
+
+            guard let pump = deviceManager?.pumpManager else {
+                save(settings)
+                return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
+            }
+            let limits = DeliveryLimits(
+                maximumBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(settings.maxBasal)),
+                maximumBolus: HKQuantity(unit: .internationalUnit(), doubleValue: Double(settings.maxBolus))
+            )
+            return Future { promise in
+                self.processQueue.async {
+                    pump.syncDeliveryLimits(limits: limits) { result in
+                        switch result {
+                        case let .success(actual):
+                            // Store the limits from the pumpManager to ensure the correct values
+                            // Example: Dana pumps don't allow to set these limits, only to fetch them
+                            // This will ensure we always have the correct values stored
+                            save(PumpSettings(
+                                insulinActionCurve: settings.insulinActionCurve,
+                                maxBolus: Decimal(
+                                    actual.maximumBolus?
+                                        .doubleValue(for: .internationalUnit()) ?? Double(settings.maxBolus)
+                                ),
+                                maxBasal: Decimal(
+                                    actual.maximumBasalRate?
+                                        .doubleValue(for: .internationalUnitsPerHour) ?? Double(settings.maxBasal)
+                                )
+                            ))
+                            promise(.success(()))
+                        case let .failure(error):
+                            promise(.failure(error))
+                        }
+                    }
+                }
+            }.eraseToAnyPublisher()
+        }
+    }
+}

+ 121 - 0
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -0,0 +1,121 @@
+import Combine
+import SwiftUI
+
+extension AlgorithmAdvancedSettings {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Injected() var storage: FileStorage!
+        @Injected() var nightscout: NightscoutManager!
+
+        @Published var units: GlucoseUnits = .mgdL
+
+        @Published var maxDailySafetyMultiplier: Decimal = 3
+        @Published var currentBasalSafetyMultiplier: Decimal = 4
+        @Published var useCustomPeakTime: Bool = false
+        @Published var insulinPeakTime: Decimal = 75
+        @Published var skipNeutralTemps: Bool = false
+        @Published var unsuspendIfNoTemp: Bool = false
+        @Published var suspendZerosIOB: Bool = false
+        @Published var min5mCarbimpact: Decimal = 8
+        @Published var autotuneISFAdjustmentFraction: Decimal = 1.0
+        @Published var remainingCarbsFraction: Decimal = 1.0
+        @Published var remainingCarbsCap: Decimal = 90
+        @Published var noisyCGMTargetMultiplier: Decimal = 1.3
+
+        @Published var insulinActionCurve: Decimal = 6
+
+        var preferences: Preferences {
+            settingsManager.preferences
+        }
+
+        var pumpSettings: PumpSettings {
+            provider.settings()
+        }
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+
+            maxDailySafetyMultiplier = settings.preferences.maxDailySafetyMultiplier
+            currentBasalSafetyMultiplier = settings.preferences.currentBasalSafetyMultiplier
+            useCustomPeakTime = settings.preferences.useCustomPeakTime
+            insulinPeakTime = settings.preferences.insulinPeakTime
+            skipNeutralTemps = settings.preferences.skipNeutralTemps
+            unsuspendIfNoTemp = settings.preferences.unsuspendIfNoTemp
+            suspendZerosIOB = settings.preferences.suspendZerosIOB
+            min5mCarbimpact = settings.preferences.min5mCarbimpact
+            autotuneISFAdjustmentFraction = settings.preferences.autotuneISFAdjustmentFraction
+            remainingCarbsFraction = settings.preferences.remainingCarbsFraction
+            remainingCarbsCap = settings.preferences.remainingCarbsCap
+            noisyCGMTargetMultiplier = settings.preferences.noisyCGMTargetMultiplier
+
+            insulinActionCurve = pumpSettings.insulinActionCurve
+        }
+
+        var isPumpSettingUnchanged: Bool {
+            pumpSettings.insulinActionCurve == insulinActionCurve
+        }
+
+        var isSettingUnchanged: Bool {
+            preferences.maxDailySafetyMultiplier == maxDailySafetyMultiplier &&
+                preferences.currentBasalSafetyMultiplier == currentBasalSafetyMultiplier &&
+                preferences.useCustomPeakTime == useCustomPeakTime &&
+                preferences.insulinPeakTime == insulinPeakTime &&
+                preferences.skipNeutralTemps == skipNeutralTemps &&
+                preferences.unsuspendIfNoTemp == unsuspendIfNoTemp &&
+                preferences.suspendZerosIOB == suspendZerosIOB &&
+                preferences.min5mCarbimpact == min5mCarbimpact &&
+                preferences.autotuneISFAdjustmentFraction == autotuneISFAdjustmentFraction &&
+                preferences.remainingCarbsFraction == remainingCarbsFraction &&
+                preferences.remainingCarbsCap == remainingCarbsCap &&
+                preferences.noisyCGMTargetMultiplier == noisyCGMTargetMultiplier
+        }
+
+        func saveIfChanged() {
+            if !isSettingUnchanged {
+                var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
+
+                newSettings.maxDailySafetyMultiplier = maxDailySafetyMultiplier
+                newSettings.currentBasalSafetyMultiplier = currentBasalSafetyMultiplier
+                newSettings.useCustomPeakTime = useCustomPeakTime
+                newSettings.insulinPeakTime = insulinPeakTime
+                newSettings.skipNeutralTemps = skipNeutralTemps
+                newSettings.unsuspendIfNoTemp = unsuspendIfNoTemp
+                newSettings.suspendZerosIOB = suspendZerosIOB
+                newSettings.min5mCarbimpact = min5mCarbimpact
+                newSettings.autotuneISFAdjustmentFraction = autotuneISFAdjustmentFraction
+                newSettings.remainingCarbsFraction = remainingCarbsFraction
+                newSettings.remainingCarbsCap = remainingCarbsCap
+                newSettings.noisyCGMTargetMultiplier = noisyCGMTargetMultiplier
+
+                newSettings.timestamp = Date()
+                storage.save(newSettings, as: OpenAPS.Settings.preferences)
+            }
+
+            if !isPumpSettingUnchanged {
+                let settings = PumpSettings(
+                    insulinActionCurve: insulinActionCurve,
+                    maxBolus: pumpSettings.maxBolus,
+                    maxBasal: pumpSettings.maxBasal
+                )
+                provider.save(settings: settings)
+                    .receive(on: DispatchQueue.main)
+                    .sink { _ in
+                        let settings = self.provider.settings()
+                        self.insulinActionCurve = settings.insulinActionCurve
+
+                        Task.detached(priority: .low) {
+                            debug(.nightscout, "Attempting to upload DIA to Nightscout")
+                            await self.nightscout.uploadProfiles()
+                        }
+                    } receiveValue: {}
+                    .store(in: &lifetime)
+            }
+        }
+    }
+}
+
+extension AlgorithmAdvancedSettings.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 325 - 0
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -0,0 +1,325 @@
+import SwiftUI
+import Swinject
+
+extension AlgorithmAdvancedSettings {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @EnvironmentObject var appIcons: Icons
+
+        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
+                )
+        }
+
+        var body: some View {
+            List {
+                Section(
+                    header: Text("DISCLAIMER"),
+                    content: {
+                        VStack(alignment: .leading) {
+                            Text(
+                                "The settings in this section are designed for advanced expert users and typically do not require ANY modifications."
+                            ).bold()
+                        }
+                    }
+
+                ).listRowBackground(Color.tabBar)
+
+                SettingInputSection(
+                    decimalValue: $state.maxDailySafetyMultiplier,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Max Daily Safety Multiplier", comment: "Max Daily Safety Multiplier")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("maxDailySafetyMultiplier"),
+                    label: NSLocalizedString("Max Daily Safety Multiplier", comment: "Max Daily Safety Multiplier"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This is an important OpenAPS safety limit. The default setting (which is unlikely to need adjusting) is 3. This means that OpenAPS will never be allowed to set a temporary basal rate that is more than 3x the highest hourly basal rate programmed in a user’s pump, or, if enabled, determined by autotune.",
+                        comment: "Max Daily Safety Multiplier"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.currentBasalSafetyMultiplier,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString(
+                                "Current Basal Safety Multiplier",
+                                comment: "Current Basal Safety Multiplier"
+                            )
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("currentBasalSafetyMultiplier"),
+                    label: NSLocalizedString("Current Basal Safety Multiplier", comment: "Current Basal Safety Multiplier"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This is another important OpenAPS safety limit. The default setting (which is also unlikely to need adjusting) is 4. This means that OpenAPS will never be allowed to set a temporary basal rate that is more than 4x the current hourly basal rate programmed in a user’s pump, or, if enabled, determined by autotune.",
+                        comment: "Current Basal Safety Multiplier"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.insulinActionCurve,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = "Duration of Insulin Action"
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("dia"),
+                    label: "Duration of Insulin Action",
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: "Duration of Insulin Action… bla bla bla"
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.insulinPeakTime,
+                    booleanValue: $state.useCustomPeakTime,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Use Custom Peak Time", comment: "Use Custom Peak Time")
+                        }
+                    ),
+                    units: state.units,
+                    type: .conditionalDecimal("insulinPeakTime"),
+                    label: NSLocalizedString("Use Custom Peak Time", comment: "Use Custom Peak Time"),
+                    conditionalLabel: NSLocalizedString("Insulin Peak Time", comment: "Insulin Peak Time"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "Defaults to false. Setting to true allows changing insulinPeakTime", comment: "Use Custom Peak Time"
+                    ) + NSLocalizedString(
+                        "Time of maximum blood glucose lowering effect of insulin, in minutes. Beware: Oref assumes for ultra-rapid (Lyumjev) & rapid-acting (Fiasp) curves minimal (35 & 50 min) and maximal (100 & 120 min) applicable insulinPeakTimes. Using a custom insulinPeakTime outside these bounds will result in issues with Trio, longer loop calculations and possible red loops.",
+                        comment: "Insulin Peak Time"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.skipNeutralTemps,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Skip Neutral Temps", comment: "Skip Neutral Temps")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: NSLocalizedString("Skip Neutral Temps", comment: "Skip Neutral Temps"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "Defaults to false, so that Trio will set temps whenever it can, so it will be easier to see if the system is working, even when you are offline. This means Trio will set a “neutral” temp (same as your default basal) if no adjustments are needed. This is an old setting for OpenAPS to have the options to minimise sounds and notifications from the 'rig', that may wake you up during the night.",
+                        comment: "Skip Neutral Temps"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.unsuspendIfNoTemp,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Unsuspend If No Temp", comment: "Unsuspend If No Temp")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: NSLocalizedString("Unsuspend If No Temp", comment: "Unsuspend If No Temp"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "Many people occasionally forget to resume / unsuspend their pump after reconnecting it. If you’re one of them, and you are willing to reliably set a zero temp basal whenever suspending and disconnecting your pump, this feature has your back. If enabled, it will automatically resume / unsuspend the pump if you forget to do so before your zero temp expires. As long as the zero temp is still running, it will leave the pump suspended.",
+                        comment: "Unsuspend If No Temp"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.suspendZerosIOB,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Suspend Zeros IOB", comment: "Suspend Zeros IOB")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: NSLocalizedString("Suspend Zeros IOB", comment: "Suspend Zeros IOB"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "Default is false. Any existing temp basals during times the pump was suspended will be deleted and 0 temp basals to negate the profile basal rates during times pump is suspended will be added.",
+                        comment: "Suspend Zeros IOB"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.autotuneISFAdjustmentFraction,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString(
+                                "Autotune ISF Adjustment Fraction",
+                                comment: "Autotune ISF Adjustment Fraction"
+                            )
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("autotuneISFAdjustmentFraction"),
+                    label: NSLocalizedString("Autotune ISF Adjustment Fraction", comment: "Autotune ISF Adjustment Fraction"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "The default of 0.5 for this value keeps autotune ISF closer to pump ISF via a weighted average of fullNewISF and pumpISF. 1.0 allows full adjustment, 0 is no adjustment from pump ISF.",
+                        comment: "Autotune ISF Adjustment Fraction"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.min5mCarbimpact,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Min 5m Carbimpact", comment: "Min 5m Carbimpact")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("min5mCarbimpact"),
+                    label: NSLocalizedString("Min 5m Carbimpact", comment: "Min 5m Carbimpact"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This is a setting for default carb absorption impact per 5 minutes. The default is an expected 8 mg/dL/5min. This affects how fast COB is decayed in situations when carb absorption is not visible in BG deviations. The default of 8 mg/dL/5min corresponds to a minimum carb absorption rate of 24g/hr at a CSF of 4 mg/dL/g.",
+                        comment: "Min 5m Carbimpact"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.remainingCarbsFraction,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Remaining Carbs Fraction", comment: "Remaining Carbs Fraction")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("remainingCarbsFraction"),
+                    label: NSLocalizedString("Remaining Carbs Fraction", comment: "Remaining Carbs Fraction"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This is the fraction of carbs we’ll assume will absorb over 4h if we don’t yet see carb absorption.",
+                        comment: "Remaining Carbs Fraction"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.remainingCarbsCap,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Remaining Carbs Cap", comment: "Remaining Carbs Cap")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("remainingCarbsCap"),
+                    label: NSLocalizedString("Remaining Carbs Cap", comment: "Remaining Carbs Cap"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This is the amount of the maximum number of carbs we’ll assume will absorb over 4h if we don’t yet see carb absorption.",
+                        comment: "Remaining Carbs Cap"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.noisyCGMTargetMultiplier,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Noisy CGM Target Multiplier", comment: "Noisy CGM Target Multiplier")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("noisyCGMTargetMultiplier"),
+                    label: NSLocalizedString("Noisy CGM Target Multiplier", comment: "Noisy CGM Target Multiplier"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "Defaults to 1.3. Increase target by this amount when looping off raw/noisy CGM data",
+                        comment: "Noisy CGM Target Multiplier"
+                    )
+                )
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationTitle("Additionals")
+            .navigationBarTitleDisplayMode(.automatic)
+            .onDisappear {
+                state.saveIfChanged()
+            }
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsDataFlow.swift

@@ -0,0 +1,5 @@
+enum AutosensSettings {
+    enum Config {}
+}
+
+protocol AutosensSettingsProvider: Provider {}

+ 3 - 0
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsProvider.swift

@@ -0,0 +1,3 @@
+extension AutosensSettings {
+    final class Provider: BaseProvider, AutosensSettingsProvider {}
+}

+ 51 - 0
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -0,0 +1,51 @@
+import SwiftUI
+
+extension AutosensSettings {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Injected() var storage: FileStorage!
+
+        @Published var units: GlucoseUnits = .mgdL
+
+        @Published var autosensMax: Decimal = 1.2
+        @Published var autosensMin: Decimal = 0.7
+        @Published var rewindResetsAutosens: Bool = true
+
+        var preferences: Preferences {
+            settingsManager.preferences
+        }
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+
+            autosensMax = settings.preferences.autosensMax
+            autosensMin = settings.preferences.autosensMin
+            rewindResetsAutosens = settings.preferences.rewindResetsAutosens
+        }
+
+        var isSettingUnchanged: Bool {
+            preferences.autosensMax == autosensMax &&
+                preferences.autosensMin == autosensMin &&
+                preferences.rewindResetsAutosens == rewindResetsAutosens
+        }
+
+        func saveIfChanged() {
+            if !isSettingUnchanged {
+                var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
+
+                newSettings.autosensMax = autosensMax
+                newSettings.autosensMin = autosensMin
+                newSettings.rewindResetsAutosens = rewindResetsAutosens
+
+                newSettings.timestamp = Date()
+                storage.save(newSettings, as: OpenAPS.Settings.preferences)
+            }
+        }
+    }
+}
+
+extension AutosensSettings.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 119 - 0
FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -0,0 +1,119 @@
+import SwiftUI
+import Swinject
+
+extension AutosensSettings {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @EnvironmentObject var appIcons: Icons
+
+        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
+                )
+        }
+
+        var body: some View {
+            List {
+                SettingInputSection(
+                    decimalValue: $state.autosensMax,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Autosens Max", comment: "Autosens Max")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("autosensMax"),
+                    label: NSLocalizedString("Autosens Max", comment: "Autosens Max"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This is a multiplier cap for autosens (and autotune) to set a 20% max limit on how high the autosens ratio can be, which in turn determines how high autosens can adjust basals, how low it can adjust ISF, and how low it can set the BG target.",
+                        comment: "Autosens Max"
+                    ),
+                    headerText: "Glucose Deviations Algorithm"
+                )
+
+                SettingInputSection(
+                    decimalValue: $state.autosensMin,
+                    booleanValue: $booleanPlaceholder,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Autosens Min", comment: "Autosens Min")
+                        }
+                    ),
+                    units: state.units,
+                    type: .decimal("autosensMin"),
+                    label: NSLocalizedString("Autosens Min", comment: "Autosens Min"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "The other side of the autosens safety limits, putting a cap on how low autosens can adjust basals, and how high it can adjust ISF and BG targets.",
+                        comment: "Autosens Min"
+                    )
+                )
+
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.rewindResetsAutosens,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = NSLocalizedString("Rewind Resets Autosens", comment: "Rewind Resets Autosens")
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: NSLocalizedString("Rewind Resets Autosens", comment: "Rewind Resets Autosens"),
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: NSLocalizedString(
+                        "This feature, enabled by default, resets the autosens ratio to neutral when you rewind your pump, on the assumption that this corresponds to a probable site change. Autosens will begin learning sensitivity anew from the time of the rewind, which may take up to 6 hours. If you usually rewind your pump independently of site changes, you may want to consider disabling this feature.",
+                        comment: "Rewind Resets Autosens"
+                    )
+                )
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationTitle("Autosens")
+            .navigationBarTitleDisplayMode(.automatic)
+            .onDisappear {
+                state.saveIfChanged()
+            }
+        }
+    }
+}

+ 6 - 0
FreeAPS/Sources/Modules/AutotuneConfig/AutotuneConfigStateModel.swift

@@ -105,3 +105,9 @@ extension AutotuneConfig {
         }
         }
     }
     }
 }
 }
+
+extension AutotuneConfig.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 84 - 20
FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift

@@ -5,6 +5,14 @@ extension AutotuneConfig {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
+
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+
         @State var replaceAlert = false
         @State var replaceAlert = false
 
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
@@ -48,22 +56,62 @@ extension AutotuneConfig {
 
 
         var body: some View {
         var body: some View {
             Form {
             Form {
-                Section {
-                    Toggle("Use Autotune", isOn: $state.useAutotune)
-                    if state.useAutotune {
-                        Toggle("Only Autotune Basal Insulin", isOn: $state.onlyAutotuneBasals)
-                    }
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.useAutotune,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = "Use Autotune"
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: "Use Autotune",
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: "Autotune… bla bla bla",
+                    headerText: "Data-driven Adjustments"
+                )
+
+                if state.useAutotune {
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.onlyAutotuneBasals,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Only Autotune Basal Insulin"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Only Autotune Basal Insulin",
+                        miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                        verboseHint: "Only Autotune Basal Insulin… bla bla bla"
+                    )
                 }
                 }
 
 
-                Section {
-                    HStack {
+                Section(
+                    header: HStack {
                         Text("Last run")
                         Text("Last run")
                         Spacer()
                         Spacer()
                         Text(dateFormatter.string(from: state.publishedDate))
                         Text(dateFormatter.string(from: state.publishedDate))
+                    },
+                    content: {
+                        Button {
+                            state.run()
+                        } label: {
+                            Text("Run now")
+                        }
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .listRowBackground(Color(.systemBlue))
+                        .tint(.white)
                     }
                     }
-                    Button { state.run() }
-                    label: { Text("Run now") }
-                }
+                )
 
 
                 if let autotune = state.autotune {
                 if let autotune = state.autotune {
                     if !state.onlyAutotuneBasals {
                     if !state.onlyAutotuneBasals {
@@ -85,6 +133,7 @@ extension AutotuneConfig {
                                 Text(state.units.rawValue + "/U").foregroundColor(.secondary)
                                 Text(state.units.rawValue + "/U").foregroundColor(.secondary)
                             }
                             }
                         }
                         }
+                        .listRowBackground(Color.chart)
                     }
                     }
 
 
                     Section(header: Text("Basal profile")) {
                     Section(header: Text("Basal profile")) {
@@ -107,27 +156,42 @@ extension AutotuneConfig {
                                 .foregroundColor(.secondary)
                                 .foregroundColor(.secondary)
                         }
                         }
                     }
                     }
+                    .listRowBackground(Color.chart)
 
 
                     Section {
                     Section {
                         Button {
                         Button {
                             Task {
                             Task {
                                 await state.delete()
                                 await state.delete()
                             }
                             }
+                        } label: {
+                            Text("Delete Autotune Data")
                         }
                         }
-                        label: { Text("Delete autotune data") }
-                            .foregroundColor(.red)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .listRowBackground(Color(.loopRed))
+                        .tint(.white)
                     }
                     }
 
 
-                    /* Section {
-                         Button {
-                             replaceAlert = true
-                         }
-                         label: { Text("Save as your Normal Basal Rates") }
-                     } header: {
-                         Text("Replace Normal Basal")
-                     } */
+                    // Section {
+                    //     Button {
+                    //         replaceAlert = true
+                    //     } label: {
+                    //         Text("Save as Normal Basal Rates")
+                    //     }
+                    //     .frame(maxWidth: .infinity, alignment: .center)
+                    //     .listRowBackground(Color(.systemGray4))
+                    //     .tint(.white)
+                    // }
                 }
                 }
             }
             }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Autotune")
             .navigationTitle("Autotune")

+ 4 - 3
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorDataFlow.swift

@@ -6,8 +6,8 @@ enum BasalProfileEditor {
 
 
     class Item: Identifiable, Hashable, Equatable {
     class Item: Identifiable, Hashable, Equatable {
         let id = UUID()
         let id = UUID()
-        var rateIndex = 0
-        var timeIndex = 0
+        var rateIndex: Int
+        var timeIndex: Int
 
 
         init(rateIndex: Int, timeIndex: Int) {
         init(rateIndex: Int, timeIndex: Int) {
             self.rateIndex = rateIndex
             self.rateIndex = rateIndex
@@ -15,10 +15,11 @@ enum BasalProfileEditor {
         }
         }
 
 
         static func == (lhs: Item, rhs: Item) -> Bool {
         static func == (lhs: Item, rhs: Item) -> Bool {
-            lhs.timeIndex == rhs.timeIndex
+            lhs.rateIndex == rhs.rateIndex && lhs.timeIndex == rhs.timeIndex
         }
         }
 
 
         func hash(into hasher: inout Hasher) {
         func hash(into hasher: inout Hasher) {
+            hasher.combine(rateIndex)
             hasher.combine(timeIndex)
             hasher.combine(timeIndex)
         }
         }
     }
     }

+ 2 - 2
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorProvider.swift

@@ -18,8 +18,8 @@ extension BasalProfileEditor {
 
 
         func saveProfile(_ profile: [BasalProfileEntry]) -> AnyPublisher<Void, Error> {
         func saveProfile(_ profile: [BasalProfileEntry]) -> AnyPublisher<Void, Error> {
             guard let pump = deviceManager?.pumpManager else {
             guard let pump = deviceManager?.pumpManager else {
-                storage.save(profile, as: OpenAPS.Settings.basalProfile)
-                return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
+                debugPrint("\(DebuggingIdentifiers.failed) No pump found; cannot save basal profile!")
+                return Fail(error: NSError()).eraseToAnyPublisher()
             }
             }
 
 
             let syncValues = profile.map {
             let syncValues = profile.map {

+ 39 - 8
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -2,9 +2,13 @@ import SwiftUI
 
 
 extension BasalProfileEditor {
 extension BasalProfileEditor {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
-        @Published var syncInProgress = false
+        @Injected() private var nightscout: NightscoutManager!
+
+        @Published var syncInProgress: Bool = false
+        @Published var initialItems: [Item] = []
         @Published var items: [Item] = []
         @Published var items: [Item] = []
         @Published var total: Decimal = 0.0
         @Published var total: Decimal = 0.0
+        @Published var showAlert: Bool = false
 
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
 
@@ -15,6 +19,10 @@ extension BasalProfileEditor {
             return lastItem.timeIndex < timeValues.count - 1
             return lastItem.timeIndex < timeValues.count - 1
         }
         }
 
 
+        var hasChanges: Bool {
+            initialItems != items
+        }
+
         override func subscribe() {
         override func subscribe() {
             rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
             rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
                 .map { ($0.decimal ?? .zero) / 100 }
                 .map { ($0.decimal ?? .zero) / 100 }
@@ -23,6 +31,9 @@ extension BasalProfileEditor {
                 let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0
                 let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
             }
+
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
             calcTotal()
             calcTotal()
         }
         }
 
 
@@ -58,21 +69,39 @@ extension BasalProfileEditor {
         }
         }
 
 
         func save() {
         func save() {
+            guard hasChanges else { return }
+
             syncInProgress = true
             syncInProgress = true
             let profile = items.map { item -> BasalProfileEntry in
             let profile = items.map { item -> BasalProfileEntry in
-                let fotmatter = DateFormatter()
-                fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
-                fotmatter.dateFormat = "HH:mm:ss"
+                let formatter = DateFormatter()
+                formatter.timeZone = TimeZone(secondsFromGMT: 0)
+                formatter.dateFormat = "HH:mm:ss"
                 let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
                 let date = Date(timeIntervalSince1970: self.timeValues[item.timeIndex])
                 let minutes = Int(date.timeIntervalSince1970 / 60)
                 let minutes = Int(date.timeIntervalSince1970 / 60)
                 let rate = self.rateValues[item.rateIndex]
                 let rate = self.rateValues[item.rateIndex]
-                return BasalProfileEntry(start: fotmatter.string(from: date), minutes: minutes, rate: rate)
+                return BasalProfileEntry(start: formatter.string(from: date), minutes: minutes, rate: rate)
             }
             }
             provider.saveProfile(profile)
             provider.saveProfile(profile)
                 .receive(on: DispatchQueue.main)
                 .receive(on: DispatchQueue.main)
-                .sink { _ in
+                .sink { completion in
                     self.syncInProgress = false
                     self.syncInProgress = false
-                } receiveValue: {}
+                    switch completion {
+                    case .finished:
+                        // Successfully saved and synced
+                        self.initialItems = self.items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
+                        Task.detached(priority: .low) {
+                            debug(.nightscout, "Attempting to upload basal rates to Nightscout")
+                            await self.nightscout.uploadProfiles()
+                        }
+                    case .failure:
+                        // Handle the error, show error message
+                        self.showAlert = true
+                    }
+                } receiveValue: {
+                    // Handle any successful value if needed
+                    print("We were successful")
+                }
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }
 
 
@@ -81,7 +110,9 @@ extension BasalProfileEditor {
                 let uniq = Array(Set(self.items))
                 let uniq = Array(Set(self.items))
                 let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
                 let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
                 sorted.first?.timeIndex = 0
                 sorted.first?.timeIndex = 0
-                self.items = sorted
+                if self.items != sorted {
+                    self.items = sorted
+                }
             }
             }
         }
         }
     }
     }

+ 64 - 45
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -40,10 +40,12 @@ extension BasalProfileEditor {
 
 
         var body: some View {
         var body: some View {
             Form {
             Form {
+                let shouldDisableButton = state.syncInProgress || state.items.isEmpty || !state.hasChanges
+
                 Section(header: Text("Schedule")) {
                 Section(header: Text("Schedule")) {
                     list
                     list
-                    addButton
-                }
+                }.listRowBackground(Color.chart)
+
                 Section {
                 Section {
                     HStack {
                     HStack {
                         Text("Total")
                         Text("Total")
@@ -55,27 +57,48 @@ extension BasalProfileEditor {
                             Text(" U/day")
                             Text(" U/day")
                             .foregroundColor(.secondary)
                             .foregroundColor(.secondary)
                     }
                     }
-                }
+                }.listRowBackground(Color.chart)
+
                 Section {
                 Section {
                     HStack {
                     HStack {
                         if state.syncInProgress {
                         if state.syncInProgress {
                             ProgressView().padding(.trailing, 10)
                             ProgressView().padding(.trailing, 10)
                         }
                         }
-                        Button { state.save() }
-                        label: {
-                            Text(state.syncInProgress ? "Saving..." : "Save on Pump")
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+                        } label: {
+                            Text(state.syncInProgress ? "Saving..." : "Save")
                         }
                         }
-                        .disabled(state.syncInProgress || state.items.isEmpty)
+                        .disabled(shouldDisableButton)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
                     }
                     }
-                }
+                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
+            }
+            .alert(isPresented: $state.showAlert) {
+                Alert(
+                    title: Text("Unable to Save"),
+                    message: Text("Trio could not communicate with your pump. Changes to your basal profile were not saved."),
+                    dismissButton: .default(Text("Close"))
+                )
+            }
+            .onChange(of: state.items) { _ in
+                state.calcTotal()
             }
             }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Basal Profile")
             .navigationTitle("Basal Profile")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
-            .navigationBarItems(
-                trailing: EditButton()
-            )
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarTrailing) {
+                    EditButton()
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    addButton
+                }
+            })
             .environment(\.editMode, $editMode)
             .environment(\.editMode, $editMode)
             .onAppear {
             .onAppear {
                 state.validate()
                 state.validate()
@@ -83,44 +106,40 @@ extension BasalProfileEditor {
         }
         }
 
 
         private func pickers(for index: Int) -> some View {
         private func pickers(for index: Int) -> some View {
-            GeometryReader { geometry in
-                VStack {
-                    HStack {
-                        Text("Rate").frame(width: geometry.size.width / 2)
-                        Text("Time").frame(width: geometry.size.width / 2)
-                    }
-                    HStack(spacing: 0) {
-                        Picker(selection: $state.items[index].rateIndex, label: EmptyView()) {
-                            ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                                Text(
-                                    (
-                                        self.rateFormatter
-                                            .string(from: state.rateValues[i] as NSNumber) ?? ""
-                                    ) + " U/hr"
-                                ).tag(i)
-                            }
+            Form {
+                Section {
+                    Picker(selection: $state.items[index].rateIndex, label: Text("Rate")) {
+                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
+                            Text(
+                                (
+                                    self.rateFormatter
+                                        .string(from: state.rateValues[i] as NSNumber) ?? ""
+                                ) + " U/hr"
+                            ).tag(i)
                         }
                         }
-                        .onChange(of: state.items[index].rateIndex, perform: { _ in state.calcTotal() })
-                        .frame(maxWidth: geometry.size.width / 2)
-                        .clipped()
+                    }
+                    .onChange(of: state.items[index].rateIndex, perform: { _ in state.calcTotal() })
+                }.listRowBackground(Color.chart)
 
 
-                        Picker(selection: $state.items[index].timeIndex, label: EmptyView()) {
-                            ForEach(0 ..< state.timeValues.count, id: \.self) { i in
-                                Text(
-                                    self.dateFormatter
-                                        .string(from: Date(
-                                            timeIntervalSince1970: state
-                                                .timeValues[i]
-                                        ))
-                                ).tag(i)
-                            }
+                Section {
+                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
+                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
+                            Text(
+                                self.dateFormatter
+                                    .string(from: Date(
+                                        timeIntervalSince1970: state
+                                            .timeValues[i]
+                                    ))
+                            ).tag(i)
                         }
                         }
-                        .onChange(of: state.items[index].timeIndex, perform: { _ in state.calcTotal() })
-                        .frame(maxWidth: geometry.size.width / 2)
-                        .clipped()
                     }
                     }
-                }
+                    .onChange(of: state.items[index].timeIndex, perform: { _ in state.calcTotal() })
+                }.listRowBackground(Color.chart)
             }
             }
+            .padding(.top)
+            .scrollContentBackground(.hidden).background(color)
+            .navigationTitle("Set Rate")
+            .navigationBarTitleDisplayMode(.automatic)
         }
         }
 
 
         private var list: some View {
         private var list: some View {
@@ -152,7 +171,7 @@ extension BasalProfileEditor {
 
 
             switch editMode {
             switch editMode {
             case .inactive:
             case .inactive:
-                return AnyView(Button(action: onAdd) { Text("Add") })
+                return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
             default:
             default:
                 return AnyView(EmptyView())
                 return AnyView(EmptyView())
             }
             }

+ 2 - 2
FreeAPS/Sources/Modules/Bolus/BolusProvider.swift

@@ -21,7 +21,7 @@ extension Bolus {
         func getBGTarget() async -> BGTargets {
         func getBGTarget() async -> BGTargets {
             await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
             await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
                 ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
                 ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
-                ?? BGTargets(units: .mgdL, userPrefferedUnits: .mgdL, targets: [])
+                ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
         }
         }
 
 
         func getISFValues() async -> InsulinSensitivities {
         func getISFValues() async -> InsulinSensitivities {
@@ -29,7 +29,7 @@ extension Bolus {
                 ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
                 ?? InsulinSensitivities(from: OpenAPS.defaults(for: OpenAPS.Settings.insulinSensitivities))
                 ?? InsulinSensitivities(
                 ?? InsulinSensitivities(
                     units: .mgdL,
                     units: .mgdL,
-                    userPrefferedUnits: .mgdL,
+                    userPreferredUnits: .mgdL,
                     sensitivities: []
                     sensitivities: []
                 )
                 )
         }
         }

+ 115 - 85
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -1,3 +1,4 @@
+import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 import LoopKit
 import LoopKit
@@ -25,7 +26,6 @@ extension Bolus {
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRequired: Decimal = 0
         @Published var insulinRequired: Decimal = 0
         @Published var units: GlucoseUnits = .mgdL
         @Published var units: GlucoseUnits = .mgdL
-        @Published var percentage: Decimal = 0
         @Published var threshold: Decimal = 0
         @Published var threshold: Decimal = 0
         @Published var maxBolus: Decimal = 0
         @Published var maxBolus: Decimal = 0
         var maxExternal: Decimal { maxBolus * 3 }
         var maxExternal: Decimal { maxBolus * 3 }
@@ -61,7 +61,6 @@ extension Bolus {
         @Published var wholeCalc: Decimal = 0
         @Published var wholeCalc: Decimal = 0
         @Published var insulinCalculated: Decimal = 0
         @Published var insulinCalculated: Decimal = 0
         @Published var fraction: Decimal = 0
         @Published var fraction: Decimal = 0
-        @Published var useCalc: Bool = false
         @Published var basal: Decimal = 0
         @Published var basal: Decimal = 0
         @Published var fattyMeals: Bool = false
         @Published var fattyMeals: Bool = false
         @Published var fattyMealFactor: Decimal = 0
         @Published var fattyMealFactor: Decimal = 0
@@ -97,7 +96,6 @@ extension Bolus {
 
 
         @Published var id_: String = ""
         @Published var id_: String = ""
         @Published var summary: String = ""
         @Published var summary: String = ""
-        @Published var skipBolus: Bool = false
 
 
         @Published var externalInsulin: Bool = false
         @Published var externalInsulin: Bool = false
         @Published var showInfo: Bool = false
         @Published var showInfo: Bool = false
@@ -111,67 +109,59 @@ extension Bolus {
         @Published var minForecast: [Int] = []
         @Published var minForecast: [Int] = []
         @Published var maxForecast: [Int] = []
         @Published var maxForecast: [Int] = []
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
         @Published var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
-        @Published var displayForecastsAsLines: Bool = false
-        @Published var smooth: Bool = false
+        @Published var forecastDisplayType: ForecastDisplayType = .cone
+        @Published var isSmoothingEnabled: Bool = false
+        @Published var stops: [Gradient.Stop] = []
 
 
         let now = Date.now
         let now = Date.now
 
 
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-        let backgroundContext = CoreDataStack.shared.newTaskContext()
+        let glucoseFetchContext = CoreDataStack.shared.newTaskContext()
+        let determinationFetchContext = CoreDataStack.shared.newTaskContext()
 
 
-        private var coreDataObserver: CoreDataObserver?
+        private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
+        private var subscriptions = Set<AnyCancellable>()
 
 
         typealias PumpEvent = PumpEventStored.EventType
         typealias PumpEvent = PumpEventStored.EventType
 
 
         override func subscribe() {
         override func subscribe() {
-            setupGlucoseNotification()
-            coreDataObserver = CoreDataObserver()
+            coreDataPublisher =
+                changedObjectsOnManagedObjectContextDidSavePublisher()
+                    .receive(on: DispatchQueue.global(qos: .background))
+                    .share()
+                    .eraseToAnyPublisher()
             registerHandlers()
             registerHandlers()
-            setupGlucoseArray()
+            registerSubscribers()
+            setupBolusStateConcurrently()
+        }
 
 
+        private func setupBolusStateConcurrently() {
             Task {
             Task {
-                async let getAllSettingsDefaults: () = getAllSettingsValues()
-                async let setupDeterminations: () = setupDeterminationsArray()
-
-                await getAllSettingsDefaults
-                await setupDeterminations
-
-                // Determination has updated, so we can use this to draw the initial Forecast Chart
-                let forecastData = await mapForecastsForChart()
-                await updateForecasts(with: forecastData)
-            }
-
-            broadcaster.register(DeterminationObserver.self, observer: self)
-            broadcaster.register(BolusFailureObserver.self, observer: self)
-            units = settingsManager.settings.units
-            percentage = settingsManager.settings.insulinReqPercentage
-            fraction = settings.settings.overrideFactor
-            useCalc = settings.settings.useCalc
-            fattyMeals = settings.settings.fattyMeals
-            fattyMealFactor = settings.settings.fattyMealFactor
-            sweetMeals = settings.settings.sweetMeals
-            sweetMealFactor = settings.settings.sweetMealFactor
-            displayPresets = settings.settings.displayPresets
-
-            displayForecastsAsLines = settings.settings.displayForecastsAsLines
-
-            lowGlucose = units == .mgdL ? settingsManager.settings.low : settingsManager.settings.low.asMmolL
-            highGlucose = units == .mgdL ? settingsManager.settings.high : settingsManager.settings.high.asMmolL
+                await withTaskGroup(of: Void.self) { group in
+                    group.addTask {
+                        self.setupGlucoseArray()
+                    }
+                    group.addTask {
+                        self.setupDeterminationsAndForecasts()
+                    }
+                    group.addTask {
+                        await self.setupSettings()
+                    }
+                    group.addTask {
+                        self.registerObservers()
+                    }
 
 
-            maxCarbs = settings.settings.maxCarbs
-            maxFat = settings.settings.maxFat
-            maxProtein = settings.settings.maxProtein
-            skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
-            useFPUconversion = settingsManager.settings.useFPUconversion
-            smooth = settingsManager.settings.smoothGlucose
-
-            if waitForSuggestionInitial {
-                Task {
-                    let ok = await apsManager.determineBasal()
-                    if !ok {
-                        self.waitForSuggestion = false
-                        self.insulinRequired = 0
-                        self.insulinRecommended = 0
+                    if self.waitForSuggestionInitial {
+                        group.addTask {
+                            let isDetermineBasalSuccessful = await self.apsManager.determineBasal()
+                            if !isDetermineBasalSuccessful {
+                                await MainActor.run {
+                                    self.waitForSuggestion = false
+                                    self.insulinRequired = 0
+                                    self.insulinRecommended = 0
+                                }
+                            }
+                        }
                     }
                     }
                 }
                 }
             }
             }
@@ -209,6 +199,43 @@ extension Bolus {
             }
             }
         }
         }
 
 
+        private func setupDeterminationsAndForecasts() {
+            Task {
+                async let getAllSettingsDefaults: () = getAllSettingsValues()
+                async let setupDeterminations: () = setupDeterminationsArray()
+
+                await getAllSettingsDefaults
+                await setupDeterminations
+
+                // Determination has updated, so we can use this to draw the initial Forecast Chart
+                let forecastData = await mapForecastsForChart()
+                await updateForecasts(with: forecastData)
+            }
+        }
+
+        private func registerObservers() {
+            broadcaster.register(DeterminationObserver.self, observer: self)
+            broadcaster.register(BolusFailureObserver.self, observer: self)
+        }
+
+        @MainActor private func setupSettings() async {
+            units = settingsManager.settings.units
+            fraction = settings.settings.overrideFactor
+            fattyMeals = settings.settings.fattyMeals
+            fattyMealFactor = settings.settings.fattyMealFactor
+            sweetMeals = settings.settings.sweetMeals
+            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
+            maxCarbs = settings.settings.maxCarbs
+            maxFat = settings.settings.maxFat
+            maxProtein = settings.settings.maxProtein
+            useFPUconversion = settingsManager.settings.useFPUconversion
+            isSmoothingEnabled = settingsManager.settings.smoothGlucose
+        }
+
         private func getCurrentSettingValue(for type: SettingType) async {
         private func getCurrentSettingValue(for type: SettingType) async {
             let now = Date()
             let now = Date()
             let calendar = Calendar.current
             let calendar = Calendar.current
@@ -284,7 +311,7 @@ extension Bolus {
 
 
         /// Calculate insulin recommendation
         /// Calculate insulin recommendation
         func calculateInsulin() -> Decimal {
         func calculateInsulin() -> Decimal {
-            let isfForCalculation = units == .mmolL ? isf.asMgdL : isf
+            let isfForCalculation = isf
 
 
             // insulin needed for the current blood glucose
             // insulin needed for the current blood glucose
             targetDifference = currentBG - target
             targetDifference = currentBG - target
@@ -363,9 +390,16 @@ extension Bolus {
 
 
                 await saveMeal()
                 await saveMeal()
 
 
-                // if glucose data is stale end the custom loading animation by hiding the modal
-                guard glucoseStorage.isGlucoseDataFresh(glucoseFromPersistence.first?.date) else {
-                    waitForSuggestion = false
+                // If glucose data is stale end the custom loading animation by hiding the modal
+                // Get date on Main thread
+                let date = await MainActor.run {
+                    glucoseFromPersistence.first?.date
+                }
+
+                guard glucoseStorage.isGlucoseDataFresh(date) else {
+                    await MainActor.run {
+                        waitForSuggestion = false
+                    }
                     return hideModal()
                     return hideModal()
                 }
                 }
             }
             }
@@ -467,7 +501,7 @@ extension Bolus {
                 enteredBy: CarbsEntry.manual,
                 enteredBy: CarbsEntry.manual,
                 isFPU: false, fpuID: UUID().uuidString
                 isFPU: false, fpuID: UUID().uuidString
             )]
             )]
-            await carbsStorage.storeCarbs(carbsToStore)
+            await carbsStorage.storeCarbs(carbsToStore, areFetchedFromRemote: false)
 
 
             if carbs > 0 || fat > 0 || protein > 0 {
             if carbs > 0 || fat > 0 || protein > 0 {
                 // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
                 // only perform determine basal sync if the user doesn't use the pump bolus, otherwise the enact bolus func in the APSManger does a sync
@@ -542,33 +576,29 @@ extension Bolus.StateModel: DeterminationObserver, BolusFailureObserver {
 
 
 extension Bolus.StateModel {
 extension Bolus.StateModel {
     private func registerHandlers() {
     private func registerHandlers() {
-        coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
+        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             Task {
             Task {
                 await self.setupDeterminationsArray()
                 await self.setupDeterminationsArray()
                 await self.updateForecasts()
                 await self.updateForecasts()
             }
             }
-        }
+        }.store(in: &subscriptions)
 
 
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
         // Due to the Batch insert this only is used for observing Deletion of Glucose entries
-        coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in
+        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
             guard let self = self else { return }
             guard let self = self else { return }
             self.setupGlucoseArray()
             self.setupGlucoseArray()
-        }
+        }.store(in: &subscriptions)
     }
     }
 
 
-    private func setupGlucoseNotification() {
-        /// custom notification that is sent when a batch insert of glucose objects is done
-        Foundation.NotificationCenter.default.addObserver(
-            self,
-            selector: #selector(handleBatchInsert),
-            name: .didPerformBatchInsert,
-            object: nil
-        )
-    }
-
-    @objc private func handleBatchInsert() {
-        setupGlucoseArray()
+    private func registerSubscribers() {
+        glucoseStorage.updatePublisher
+            .receive(on: DispatchQueue.global(qos: .background))
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupGlucoseArray()
+            }
+            .store(in: &subscriptions)
     }
     }
 }
 }
 
 
@@ -587,16 +617,16 @@ extension Bolus.StateModel {
     private func fetchGlucose() async -> [NSManagedObjectID] {
     private func fetchGlucose() async -> [NSManagedObjectID] {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             ofType: GlucoseStored.self,
-            onContext: backgroundContext,
+            onContext: glucoseFetchContext,
             predicate: NSPredicate.glucose,
             predicate: NSPredicate.glucose,
             key: "date",
             key: "date",
             ascending: false,
             ascending: false,
             fetchLimit: 288
             fetchLimit: 288
         )
         )
 
 
-        guard let fetchedResults = results as? [GlucoseStored] else { return [] }
+        return await glucoseFetchContext.perform {
+            guard let fetchedResults = results as? [GlucoseStored] else { return [] }
 
 
-        return await backgroundContext.perform {
             return fetchedResults.map(\.objectID)
             return fetchedResults.map(\.objectID)
         }
         }
     }
     }
@@ -632,16 +662,16 @@ extension Bolus.StateModel {
 
 
     private func mapForecastsForChart() async -> Determination? {
     private func mapForecastsForChart() async -> Determination? {
         let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
         let determinationObjects: [OrefDetermination] = await CoreDataStack.shared
-            .getNSManagedObject(with: determinationObjectIDs, context: backgroundContext)
+            .getNSManagedObject(with: determinationObjectIDs, context: determinationFetchContext)
 
 
-        return await backgroundContext.perform {
+        return await determinationFetchContext.perform {
             guard let determinationObject = determinationObjects.first else {
             guard let determinationObject = determinationObjects.first else {
                 return nil
                 return nil
             }
             }
 
 
             let eventualBG = determinationObject.eventualBG?.intValue
             let eventualBG = determinationObject.eventualBG?.intValue
 
 
-            let forecastsSet = determinationObject.forecasts as? Set<Forecast> ?? []
+            let forecastsSet = determinationObject.forecasts ?? []
             let predictions = Predictions(
             let predictions = Predictions(
                 iob: forecastsSet.extractValues(for: "iob"),
                 iob: forecastsSet.extractValues(for: "iob"),
                 zt: forecastsSet.extractValues(for: "zt"),
                 zt: forecastsSet.extractValues(for: "zt"),
@@ -730,20 +760,20 @@ extension Bolus.StateModel {
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         minCount = max(12, nonEmptyArrays.map(\.count).min() ?? 0)
         guard minCount > 0 else { return }
         guard minCount > 0 else { return }
 
 
-        let (minResult, maxResult) = await Task.detached {
-            let minForecast = (0 ..< self.minCount).map { index in
+        async let minForecastResult = Task.detached {
+            (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.min() ?? 0
             }
             }
+        }.value
 
 
-            let maxForecast = (0 ..< self.minCount).map { index in
+        async let maxForecastResult = Task.detached {
+            (0 ..< self.minCount).map { index in
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
                 nonEmptyArrays.compactMap { $0.indices.contains(index) ? $0[index] : nil }.max() ?? 0
             }
             }
-
-            return (minForecast, maxForecast)
         }.value
         }.value
 
 
-        minForecast = minResult
-        maxForecast = maxResult
+        minForecast = await minForecastResult
+        maxForecast = await maxForecastResult
     }
     }
 }
 }
 
 

+ 57 - 66
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -171,39 +171,48 @@ extension Bolus {
                 VStack {
                 VStack {
                     Form {
                     Form {
                         Section {
                         Section {
+                            ForeCastChart(state: state, units: $state.units)
+                                .padding(.vertical)
+                        }.listRowBackground(Color.chart)
+
+                        Section {
                             carbsTextField()
                             carbsTextField()
 
 
-                            if state.useFPUconversion {
-                                proteinAndFat()
-                            }
+                            DisclosureGroup("Extras") {
+                                if state.useFPUconversion {
+                                    proteinAndFat()
+                                }
 
 
-                            // Time
-                            HStack {
-                                Text("Time").foregroundStyle(Color.secondary)
-                                Spacer()
-                                if !pushed {
-                                    Button {
-                                        pushed = true
-                                    } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
-                                        .padding(.trailing, 5)
-                                } else {
-                                    Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
-                                    label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
-                                    DatePicker(
-                                        "Time",
-                                        selection: $state.date,
-                                        displayedComponents: [.hourAndMinute]
-                                    ).controlSize(.mini)
-                                        .labelsHidden()
-                                    Button {
-                                        state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                // Time
+                                HStack {
+                                    Text("Time").foregroundStyle(Color.secondary)
+                                    Spacer()
+                                    if !pushed {
+                                        Button {
+                                            pushed = true
+                                        } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
+                                            .padding(.trailing, 5)
+                                    } else {
+                                        Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
+                                        label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+                                        DatePicker(
+                                            "Time",
+                                            selection: $state.date,
+                                            displayedComponents: [.hourAndMinute]
+                                        ).controlSize(.mini)
+                                            .labelsHidden()
+                                        Button {
+                                            state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                        }
+                                        label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                     }
                                     }
-                                    label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
                                 }
                                 }
-                            }
-                            HStack {
-                                Image(systemName: "square.and.pencil").foregroundColor(.secondary)
-                                TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+
+                                // Notes
+                                HStack {
+                                    Image(systemName: "square.and.pencil").foregroundColor(.secondary)
+                                    TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+                                }
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
@@ -293,15 +302,10 @@ extension Bolus {
                             }
                             }
                         }.listRowBackground(Color.chart)
                         }.listRowBackground(Color.chart)
 
 
-                        Section {
-                            ForeCastChart(state: state, units: $state.units)
-                                .padding(.vertical)
-                        }.listRowBackground(Color.chart)
+                        treatmentButton
                     }
                     }
                 }
                 }
-                .safeAreaInset(edge: .bottom, spacing: 0) {
-                    stickyButton
-                }.blur(radius: state.waitForSuggestion ? 5 : 0)
+                .blur(radius: state.waitForSuggestion ? 5 : 0)
 
 
                 if state.waitForSuggestion {
                 if state.waitForSuggestion {
                     CustomProgressView(text: progressText.rawValue)
                     CustomProgressView(text: progressText.rawValue)
@@ -365,46 +369,33 @@ extension Bolus {
             }
             }
         }
         }
 
 
-        var stickyButton: some View {
-            ZStack {
-                Rectangle()
-                    .frame(width: UIScreen.main.bounds.width, height: 120).offset(y: 40)
-                    .shadow(
-                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
-                            Color.black.opacity(0.33),
-                        radius: 3
-                    )
-                    .foregroundStyle(Color.chart)
-
-                Button {
-                    state.invokeTreatmentsTask()
-                } label: {
-                    taskButtonLabel
-                        .font(.headline)
-                        .foregroundStyle(Color.white)
-                        .frame(maxWidth: .infinity, alignment: .center)
-                        .frame(minHeight: 50)
-                }
-                .disabled(disableTaskButton)
-                .background(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
-                .shadow(radius: 3)
-                .clipShape(RoundedRectangle(cornerRadius: 8))
-                .padding()
-                .offset(y: 20)
+        var treatmentButton: some View {
+            Button {
+                state.invokeTreatmentsTask()
+            } label: {
+                taskButtonLabel
+                    .font(.headline)
+                    .foregroundStyle(Color.white)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .frame(height: 35)
             }
             }
+            .disabled(disableTaskButton)
+            .listRowBackground(limitExceeded ? Color(.systemRed) : Color(.systemBlue))
+            .shadow(radius: 3)
+            .clipShape(RoundedRectangle(cornerRadius: 8))
         }
         }
 
 
         private var taskButtonLabel: some View {
         private var taskButtonLabel: some View {
             if pumpBolusLimitExceeded {
             if pumpBolusLimitExceeded {
-                return Text("Max Bolus of \(state.maxBolus) U Exceeded")
+                return Text("Max Bolus of \(state.maxBolus.description) U Exceeded")
             } else if externalBolusLimitExceeded {
             } else if externalBolusLimitExceeded {
-                return Text("Max External Bolus of \(state.maxExternal) U Exceeded")
+                return Text("Max External Bolus of \(state.maxExternal.description) U Exceeded")
             } else if carbLimitExceeded {
             } else if carbLimitExceeded {
-                return Text("Max Carbs of \(state.maxCarbs) g Exceeded")
+                return Text("Max Carbs of \(state.maxCarbs.description) g Exceeded")
             } else if fatLimitExceeded {
             } else if fatLimitExceeded {
-                return Text("Max Fat of \(state.maxFat) g Exceeded")
+                return Text("Max Fat of \(state.maxFat.description) g Exceeded")
             } else if proteinLimitExceeded {
             } else if proteinLimitExceeded {
-                return Text("Max Protein of \(state.maxProtein) g Exceeded")
+                return Text("Max Protein of \(state.maxProtein.description) g Exceeded")
             }
             }
 
 
             let hasInsulin = state.amount > 0
             let hasInsulin = state.amount > 0

+ 57 - 65
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -12,7 +12,7 @@ struct ForeCastChart: View {
 
 
     private var endMarker: Date {
     private var endMarker: Date {
         state
         state
-            .displayForecastsAsLines ? Date(timeIntervalSinceNow: TimeInterval(hours: 3)) :
+            .forecastDisplayType == .lines ? Date(timeIntervalSinceNow: TimeInterval(hours: 3)) :
             Date(timeIntervalSinceNow: TimeInterval(
             Date(timeIntervalSinceNow: TimeInterval(
                 Int(1.5) * 5 * state
                 Int(1.5) * 5 * state
                     .minCount * 60
                     .minCount * 60
@@ -35,6 +35,42 @@ struct ForeCastChart: View {
 
 
     var body: some View {
     var body: some View {
         VStack {
         VStack {
+            HStack {
+                HStack {
+                    Text("Added carbs: ")
+                        .font(.footnote)
+                        .fontWeight(.bold)
+                        .foregroundStyle(.orange)
+
+                    Text("\(state.carbs.description) g")
+                        .font(.footnote)
+                        .foregroundStyle(.orange)
+                }
+                .padding(8)
+                .background {
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(Color.orange.opacity(0.2))
+                }
+
+                Spacer()
+
+                HStack {
+                    Text("Added insulin: ")
+                        .font(.footnote)
+                        .fontWeight(.bold)
+                        .foregroundStyle(.blue)
+
+                    Text("\(state.amount.description) U")
+                        .font(.footnote)
+                        .foregroundStyle(.blue)
+                }
+                .padding(8)
+                .background {
+                    RoundedRectangle(cornerRadius: 10)
+                        .fill(Color.blue.opacity(0.2))
+                }
+            }
+
             forecastChart
             forecastChart
                 .padding(.vertical, 3)
                 .padding(.vertical, 3)
             HStack {
             HStack {
@@ -70,7 +106,7 @@ struct ForeCastChart: View {
             drawGlucose()
             drawGlucose()
             drawCurrentTimeMarker()
             drawCurrentTimeMarker()
 
 
-            if state.displayForecastsAsLines {
+            if state.forecastDisplayType == .lines {
                 drawForecastLines()
                 drawForecastLines()
             } else {
             } else {
                 drawForecastsCone()
                 drawForecastsCone()
@@ -83,74 +119,30 @@ struct ForeCastChart: View {
         .backport.chartForegroundStyleScale(state: state)
         .backport.chartForegroundStyleScale(state: state)
     }
     }
 
 
-    private var stops: [Gradient.Stop] {
-        let low = Double(state.lowGlucose)
-        let high = Double(state.highGlucose)
-
-        let glucoseValues = state.glucoseFromPersistence
-            .map { units == .mgdL ? Decimal($0.glucose) : Decimal($0.glucose).asMmolL }
-
-        let minimum = glucoseValues.min() ?? 0.0
-        let maximum = glucoseValues.max() ?? 0.0
-
-        // Calculate positions for gradient
-        let lowPosition = (low - Double(truncating: minimum as NSNumber)) /
-            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
-        let highPosition = (high - Double(truncating: minimum as NSNumber)) /
-            (Double(truncating: maximum as NSNumber) - Double(truncating: minimum as NSNumber))
-
-        // Ensure positions are in bounds [0, 1]
-        let clampedLowPosition = max(0.0, min(lowPosition, 1.0))
-        let clampedHighPosition = max(0.0, min(highPosition, 1.0))
-
-        // Ensure lowPosition is less than highPosition
-        let sortedPositions = [clampedLowPosition, clampedHighPosition].sorted()
-
-        return [
-            Gradient.Stop(color: .red, location: 0.0),
-            Gradient.Stop(color: .red, location: sortedPositions[0]), // draw red gradient till lowGlucose
-            Gradient.Stop(color: .green, location: sortedPositions[0] + 0.0001), // draw green above lowGlucose till highGlucose
-            Gradient.Stop(color: .green, location: sortedPositions[1]),
-            Gradient.Stop(color: .orange, location: sortedPositions[1] + 0.0001), // draw orange above highGlucose
-            Gradient.Stop(color: .orange, location: 1.0)
-        ]
-    }
-
     private func drawGlucose() -> some ChartContent {
     private func drawGlucose() -> some ChartContent {
         ForEach(state.glucoseFromPersistence) { item in
         ForEach(state.glucoseFromPersistence) { item in
-            let glucoseToDisplay = units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
-
-            if state.smooth {
-                LineMark(
-                    x: .value("Time", item.date ?? Date()),
+            let glucoseToDisplay = state.units == .mgdL ? Decimal(item.glucose) : Decimal(item.glucose).asMmolL
+            let pointMarkColor: Color = glucoseToDisplay > state.highGlucose ? Color.orange :
+                glucoseToDisplay < state.lowGlucose ? Color.red :
+                Color.green
+
+            if !state.isSmoothingEnabled {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
                     y: .value("Value", glucoseToDisplay)
                     y: .value("Value", glucoseToDisplay)
                 )
                 )
-                .foregroundStyle(
-                    .linearGradient(stops: stops, startPoint: .bottom, endPoint: .top)
-                )
-                .symbol(.circle).symbolSize(34)
+                .foregroundStyle(pointMarkColor)
+                .symbolSize(20)
             } else {
             } else {
-                if item.glucose > Int(state.highGlucose) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", glucoseToDisplay)
-                    )
-                    .foregroundStyle(Color.orange.gradient)
-                    .symbolSize(20)
-                } else if item.glucose < Int(state.lowGlucose) {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", glucoseToDisplay)
-                    )
-                    .foregroundStyle(Color.red.gradient)
-                    .symbolSize(20)
-                } else {
-                    PointMark(
-                        x: .value("Time", item.date ?? Date(), unit: .second),
-                        y: .value("Value", glucoseToDisplay)
-                    )
-                    .foregroundStyle(Color.green.gradient)
-                    .symbolSize(20)
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", glucoseToDisplay)
+                )
+                .symbol {
+                    Image(systemName: "record.circle.fill")
+                        .font(.system(size: 8))
+                        .bold()
+                        .foregroundStyle(pointMarkColor)
                 }
                 }
             }
             }
         }
         }

+ 45 - 58
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -134,14 +134,15 @@ struct PopupView: View {
             Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
             Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
                 .gridCellAnchor(.leading)
                 .gridCellAnchor(.leading)
 
 
+            let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
             Text(
             Text(
-                state.isf.formatted() + " " + state.units
+                isf + " " + state.units
                     .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
                     .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
             ).gridCellAnchor(.leading)
             ).gridCellAnchor(.leading)
-            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+
+            let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
             Text(
             Text(
-                target
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                target +
                     " " + state.units.rawValue
                     " " + state.units.rawValue
             ).gridCellAnchor(.leading)
             ).gridCellAnchor(.leading)
         }
         }
@@ -149,28 +150,25 @@ struct PopupView: View {
 
 
     var calcGlucoseFirstRow: some View {
     var calcGlucoseFirstRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
-            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+            let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
+            let target = state.units == .mmolL ? state.target.formattedAsMmolL : state.target.description
 
 
             Text("Glucose:").foregroundColor(.secondary)
             Text("Glucose:").foregroundColor(.secondary)
 
 
-            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
+            let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
+                .description
             let firstRow = currentBG
             let firstRow = currentBG
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-
                 + " - " +
                 + " - " +
                 target
                 target
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
                 + " = " +
                 + " = " +
                 targetDifference
                 targetDifference
-                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
 
 
             Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
             Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
                 .gridColumnAlignment(.leading)
                 .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
                 Text(
                 Text(
-                    self.insulinRounder(state.targetDifferenceInsulin).formatted()
+                    self.insulinFormatter(state.targetDifferenceInsulin)
                 )
                 )
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
@@ -180,24 +178,18 @@ struct PopupView: View {
 
 
     var calcGlucoseSecondRow: some View {
     var calcGlucoseSecondRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+            let currentBG = state.units == .mmolL ? state.currentBG.formattedAsMmolL : state.currentBG.description
             Text(
             Text(
                 currentBG
                 currentBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    " " +
+                    + " " +
                     state.units.rawValue
                     state.units.rawValue
             )
             )
 
 
-            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
-            let secondRow = targetDifference
-                .formatted(
-                    .number.grouping(.never).rounded()
-                        .precision(.fractionLength(fractionDigits))
-                )
-                + " / " +
-                state.isf.formatted()
-                + " ≈ " +
-                self.insulinRounder(state.targetDifferenceInsulin).formatted()
+            let targetDifference = state.units == .mmolL ? state.targetDifference.formattedAsMmolL : state.targetDifference
+                .description
+            let secondRow = targetDifference + " / " +
+                (state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description)
+                .description + " ≈ " + self.insulinFormatter(state.targetDifferenceInsulin)
 
 
             Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
             Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
 
 
@@ -221,13 +213,13 @@ struct PopupView: View {
             HStack {
             HStack {
                 Text("IOB:").foregroundColor(.secondary)
                 Text("IOB:").foregroundColor(.secondary)
                 Text(
                 Text(
-                    self.insulinRounder(state.iob).formatted()
+                    self.insulinFormatter(state.iob)
                 )
                 )
             }
             }
 
 
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
-            let iobFormatted = self.insulinRounder(state.iob).formatted()
+            let iobFormatted = self.insulinFormatter(state.iob)
             HStack {
             HStack {
                 Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
                 Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -253,14 +245,14 @@ struct PopupView: View {
                     + " / " +
                     + " / " +
                     state.carbRatio.formatted()
                     state.carbRatio.formatted()
                     + " ≈ " +
                     + " ≈ " +
-                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                    self.insulinFormatter(state.wholeCobInsulin)
             )
             )
             .foregroundColor(.secondary)
             .foregroundColor(.secondary)
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
                 Text(
                 Text(
-                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                    self.insulinFormatter(state.wholeCobInsulin)
                 )
                 )
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
@@ -283,25 +275,19 @@ struct PopupView: View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
             Text("Delta:").foregroundColor(.secondary)
             Text("Delta:").foregroundColor(.secondary)
 
 
-            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
+            let isf = state.units == .mmolL ? state.isf.formattedAsMmolL : state.isf.description
+
+            let fifteenMinInsulinFormatted = self.insulinFormatter(state.fifteenMinInsulin)
+
             Text(
             Text(
-                deltaBG
-                    .formatted(
-                        .number.grouping(.never).rounded()
-                            .precision(.fractionLength(fractionDigits))
-                    )
-                    + " / " +
-                    state.isf.formatted()
-                    + " ≈ " +
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
+                deltaBG + " / " + isf + " ≈ " + fifteenMinInsulinFormatted
             )
             )
             .foregroundColor(.secondary)
             .foregroundColor(.secondary)
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
-                Text(
-                    self.insulinRounder(state.fifteenMinInsulin).formatted()
-                )
+                Text(fifteenMinInsulinFormatted)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.fontWeight(.bold)
             }.fontWeight(.bold)
                 .gridColumnAlignment(.trailing)
                 .gridColumnAlignment(.trailing)
@@ -310,13 +296,10 @@ struct PopupView: View {
 
 
     var calcDeltaFormulaRow: some View {
     var calcDeltaFormulaRow: some View {
         GridRow(alignment: .center) {
         GridRow(alignment: .center) {
-            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            let deltaBG = state.units == .mmolL ? state.deltaBG.formattedAsMmolL : state.deltaBG.description
             Text(
             Text(
                 deltaBG
                 deltaBG
-                    .formatted(
-                        .number.grouping(.never).rounded()
-                            .precision(.fractionLength(fractionDigits))
-                    ) + " " +
+                    + " " +
                     state.units.rawValue
                     state.units.rawValue
             )
             )
 
 
@@ -334,7 +317,7 @@ struct PopupView: View {
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
             Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
 
 
             HStack {
             HStack {
-                Text(self.insulinRounder(state.wholeCalc).formatted())
+                Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.gridColumnAlignment(.trailing)
             }.gridColumnAlignment(.trailing)
@@ -350,7 +333,7 @@ struct PopupView: View {
             Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
             Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
 
 
             HStack {
             HStack {
-                Text("+" + self.insulinRounder(state.superBolusInsulin).formatted())
+                Text("+" + self.insulinFormatter(state.superBolusInsulin))
                     .foregroundStyle(Color.loopRed)
                     .foregroundStyle(Color.loopRed)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
             }.gridColumnAlignment(.trailing)
             }.gridColumnAlignment(.trailing)
@@ -379,7 +362,7 @@ struct PopupView: View {
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
                     // endif fatty meal is chosen
                     // endif fatty meal is chosen
 
 
-                    + Text(self.insulinRounder(state.wholeCalc).formatted())
+                    + Text(self.insulinFormatter(state.wholeCalc))
                     .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
                     .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
 
 
                     // if superbolus is chosen
                     // if superbolus is chosen
@@ -389,7 +372,7 @@ struct PopupView: View {
                     + Text(state.useSuperBolus ? " + " : "")
                     + Text(state.useSuperBolus ? " + " : "")
                     .foregroundColor(.secondary)
                     .foregroundColor(.secondary)
 
 
-                    + Text(state.useSuperBolus ? state.superBolusInsulin.formatted() : "")
+                    + Text(state.useSuperBolus ? self.insulinFormatter(state.superBolusInsulin) : "")
                     .foregroundColor(.loopRed)
                     .foregroundColor(.loopRed)
                     // endif superbolus is chosen
                     // endif superbolus is chosen
 
 
@@ -399,7 +382,7 @@ struct PopupView: View {
             .gridColumnAlignment(.leading)
             .gridColumnAlignment(.leading)
 
 
             HStack {
             HStack {
-                Text(self.insulinRounder(state.insulinCalculated).formatted())
+                Text(self.insulinFormatter(state.insulinCalculated))
                     .fontWeight(.bold)
                     .fontWeight(.bold)
                     .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
                     .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
                 Text("U").foregroundColor(.secondary)
                 Text("U").foregroundColor(.secondary)
@@ -447,9 +430,17 @@ struct PopupView: View {
         }
         }
     }
     }
 
 
-    private func insulinRounder(_ value: Decimal) -> Decimal {
+    private func insulinFormatter(_ value: Decimal) -> String {
         let toRound = NSDecimalNumber(decimal: value).doubleValue
         let toRound = NSDecimalNumber(decimal: value).doubleValue
-        return Decimal(floor(100 * toRound) / 100)
+        let roundedValue = Decimal(floor(100 * toRound) / 100)
+
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumFractionDigits = 2
+        formatter.maximumFractionDigits = 2
+        formatter.locale = Locale.current // Uses the user's locale
+
+        return formatter.string(from: roundedValue as NSNumber) ?? String(format: "%.2f", toRound)
     }
     }
 
 
     struct DividerDouble: View {
     struct DividerDouble: View {
@@ -476,7 +467,3 @@ struct PopupView: View {
         }
         }
     }
     }
 }
 }
-
-// #Preview {
-//    PopupView()
-// }

+ 9 - 4
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -2,23 +2,23 @@ import SwiftUI
 
 
 extension BolusCalculatorConfig {
 extension BolusCalculatorConfig {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
+        @Published var units: GlucoseUnits = .mgdL
         @Published var overrideFactor: Decimal = 0
         @Published var overrideFactor: Decimal = 0
-        @Published var useCalc: Bool = false
         @Published var fattyMeals: Bool = false
         @Published var fattyMeals: Bool = false
         @Published var fattyMealFactor: Decimal = 0
         @Published var fattyMealFactor: Decimal = 0
         @Published var sweetMeals: Bool = false
         @Published var sweetMeals: Bool = false
         @Published var sweetMealFactor: Decimal = 0
         @Published var sweetMealFactor: Decimal = 0
-        @Published var insulinReqPercentage: Decimal = 70
         @Published var displayPresets: Bool = true
         @Published var displayPresets: Bool = true
 
 
         override func subscribe() {
         override func subscribe() {
+            units = settingsManager.settings.units
+
             subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
             subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
                 let value = max(min($0, 1.2), 0.1)
                 let value = max(min($0, 1.2), 0.1)
                 overrideFactor = value
                 overrideFactor = value
             }, map: {
             }, map: {
                 $0
                 $0
             })
             })
-            subscribeSetting(\.useCalc, on: $useCalc) { useCalc = $0 }
             subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
             subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
             subscribeSetting(\.displayPresets, on: $displayPresets) { displayPresets = $0 }
             subscribeSetting(\.displayPresets, on: $displayPresets) { displayPresets = $0 }
             subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
             subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
@@ -34,7 +34,12 @@ extension BolusCalculatorConfig {
             }, map: {
             }, map: {
                 $0
                 $0
             })
             })
-            subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
         }
         }
     }
     }
 }
 }
+
+extension BolusCalculatorConfig.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 84 - 60
FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift


+ 29 - 42
FreeAPS/Sources/Modules/CGM/CGMStateModel.swift

@@ -21,32 +21,43 @@ let cgmDefaultName = cgmName(
 extension CGM {
 extension CGM {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var cgmManager: FetchGlucoseManager!
         @Injected() var cgmManager: FetchGlucoseManager!
-        @Injected() var calendarManager: CalendarManager!
         @Injected() var pluginCGMManager: PluginManager!
         @Injected() var pluginCGMManager: PluginManager!
         @Injected() private var broadcaster: Broadcaster!
         @Injected() private var broadcaster: Broadcaster!
         @Injected() var nightscoutManager: NightscoutManager!
         @Injected() var nightscoutManager: NightscoutManager!
 
 
+        @Published var units: GlucoseUnits = .mgdL
         @Published var setupCGM: Bool = false
         @Published var setupCGM: Bool = false
         @Published var cgmCurrent = cgmDefaultName
         @Published var cgmCurrent = cgmDefaultName
         @Published var smoothGlucose = false
         @Published var smoothGlucose = false
-        @Published var createCalendarEvents = false
-        @Published var displayCalendarIOBandCOB = false
-        @Published var displayCalendarEmojis = false
-        @Published var calendarIDs: [String] = []
-        @Published var currentCalendarID: String = ""
-        @Persisted(key: "CalendarManager.currentCalendarID") var storedCalendarID: String? = nil
         @Published var cgmTransmitterDeviceAddress: String? = nil
         @Published var cgmTransmitterDeviceAddress: String? = nil
         @Published var listOfCGM: [cgmName] = []
         @Published var listOfCGM: [cgmName] = []
         @Published var url: URL?
         @Published var url: URL?
 
 
         override func subscribe() {
         override func subscribe() {
+            units = settingsManager.settings.units
+
             // collect the list of CGM available with plugins and CGMType defined manually
             // collect the list of CGM available with plugins and CGMType defined manually
-            listOfCGM = CGMType.allCases.filter { $0 != CGMType.plugin }.map {
-                cgmName(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
-            } +
-                pluginCGMManager.availableCGMManagers.map {
-                    cgmName(id: $0.identifier, type: CGMType.plugin, displayName: $0.localizedTitle, subtitle: $0.localizedTitle)
+            listOfCGM = (
+                CGMType.allCases.filter { $0 != CGMType.plugin }.map {
+                    cgmName(id: $0.id, type: $0, displayName: $0.displayName, subtitle: $0.subtitle)
+                } +
+                    pluginCGMManager.availableCGMManagers.map {
+                        cgmName(
+                            id: $0.identifier,
+                            type: CGMType.plugin,
+                            displayName: $0.localizedTitle,
+                            subtitle: $0.localizedTitle
+                        )
+                    }
+            ).sorted(by: { lhs, rhs in
+                if lhs.displayName == "None" {
+                    return true
+                } else if rhs.displayName == "None" {
+                    return false
+                } else {
+                    return lhs.displayName < rhs.displayName
                 }
                 }
+            })
 
 
             switch settingsManager.settings.cgm {
             switch settingsManager.settings.cgm {
             case .plugin:
             case .plugin:
@@ -81,13 +92,8 @@ extension CGM {
             default: break
             default: break
             }
             }
 
 
-            currentCalendarID = storedCalendarID ?? ""
-            calendarIDs = calendarManager.calendarIDs()
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
             cgmTransmitterDeviceAddress = UserDefaults.standard.cgmTransmitterDeviceAddress
 
 
-            subscribeSetting(\.useCalendar, on: $createCalendarEvents) { createCalendarEvents = $0 }
-            subscribeSetting(\.displayCalendarIOBandCOB, on: $displayCalendarIOBandCOB) { displayCalendarIOBandCOB = $0 }
-            subscribeSetting(\.displayCalendarEmojis, on: $displayCalendarEmojis) { displayCalendarEmojis = $0 }
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
             subscribeSetting(\.smoothGlucose, on: $smoothGlucose, initial: { smoothGlucose = $0 })
 
 
             $cgmCurrent
             $cgmCurrent
@@ -111,31 +117,6 @@ extension CGM {
                     }
                     }
                 }
                 }
                 .store(in: &lifetime)
                 .store(in: &lifetime)
-
-            $createCalendarEvents
-                .removeDuplicates()
-                .flatMap { [weak self] ok -> AnyPublisher<Bool, Never> in
-                    guard ok, let self = self else { return Just(false).eraseToAnyPublisher() }
-                    return self.calendarManager.requestAccessIfNeeded()
-                }
-                .map { [weak self] ok -> [String] in
-                    guard ok, let self = self else { return [] }
-                    return self.calendarManager.calendarIDs()
-                }
-                .receive(on: DispatchQueue.main)
-                .weakAssign(to: \.calendarIDs, on: self)
-                .store(in: &lifetime)
-
-            $currentCalendarID
-                .removeDuplicates()
-                .sink { [weak self] id in
-                    guard id.isNotEmpty else {
-                        self?.calendarManager.currentCalendarID = nil
-                        return
-                    }
-                    self?.calendarManager.currentCalendarID = id
-                }
-                .store(in: &lifetime)
         }
         }
 
 
         func displayNameOfApp() -> String? {
         func displayNameOfApp() -> String? {
@@ -199,3 +180,9 @@ extension CGM.StateModel: CGMManagerOnboardingDelegate {
         // nothing to do ?
         // nothing to do ?
     }
     }
 }
 }
+
+extension CGM.StateModel {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 128 - 64
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -9,6 +9,13 @@ extension CGM {
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
         @State private var setupCGM = false
         @State private var setupCGM = false
 
 
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.colorScheme) var colorScheme
         var color: LinearGradient {
         var color: LinearGradient {
             colorScheme == .dark ? LinearGradient(
             colorScheme == .dark ? LinearGradient(
@@ -27,29 +34,75 @@ extension CGM {
                 )
                 )
         }
         }
 
 
-        // @AppStorage(UserDefaults.BTKey.cgmTransmitterDeviceAddress.rawValue) private var cgmTransmitterDeviceAddress: String? = nil
-
         var body: some View {
         var body: some View {
             NavigationView {
             NavigationView {
                 Form {
                 Form {
-                    Section(header: Text("CGM")) {
-                        Picker("Type", selection: $state.cgmCurrent) {
-                            ForEach(state.listOfCGM) { type in
-                                VStack(alignment: .leading) {
-                                    Text(type.displayName)
-                                    Text(type.subtitle).font(.caption).foregroundColor(.secondary)
-                                }.tag(type)
+                    Section(
+                        header: Text("CGM Integration to Trio"),
+                        content: {
+                            VStack {
+                                Picker("Type", selection: $state.cgmCurrent) {
+                                    ForEach(state.listOfCGM) { type in
+                                        VStack(alignment: .leading) {
+                                            Text(type.displayName)
+                                            Text(type.subtitle).font(.caption).foregroundColor(.secondary)
+                                        }.tag(type)
+                                    }
+                                }.padding(.top)
+
+                                HStack(alignment: .top) {
+                                    Text(
+                                        "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                    )
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                                    .lineLimit(nil)
+                                    Spacer()
+                                    Button(
+                                        action: {
+                                            hintLabel = "Available CGM Types for Trio"
+                                            selectedVerboseHint =
+                                                "CGM Types… bla bla \n\nLorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                            shouldDisplayHint.toggle()
+                                        },
+                                        label: {
+                                            HStack {
+                                                Image(systemName: "questionmark.circle")
+                                            }
+                                        }
+                                    ).buttonStyle(BorderlessButtonStyle())
+                                }.padding(.top)
+                            }.padding(.bottom)
+
+                            if let link = state.cgmCurrent.type.externalLink {
+                                Button {
+                                    UIApplication.shared.open(link, options: [:], completionHandler: nil)
+                                } label: {
+                                    HStack {
+                                        Text("About this source")
+                                        Spacer()
+                                        Image(systemName: "chevron.right")
+                                    }
+                                }
+                                .frame(maxWidth: .infinity, alignment: .leading)
                             }
                             }
-                        }
-                        if let link = state.cgmCurrent.type.externalLink {
-                            Button("About this source") {
-                                UIApplication.shared.open(link, options: [:], completionHandler: nil)
+
+                            if state.cgmCurrent.type == .plugin {
+                                Button {
+                                    setupCGM.toggle()
+                                } label: {
+                                    HStack {
+                                        Text("CGM Configuration")
+                                        Spacer()
+                                        Image(systemName: "chevron.right")
+                                    }
+                                }
+                                .frame(maxWidth: .infinity, alignment: .leading)
                             }
                             }
                         }
                         }
-                    }
+                    ).listRowBackground(Color.chart)
 
 
-                    if let appURL = state.urlOfApp()
-                    {
+                    if let appURL = state.urlOfApp() {
                         Section {
                         Section {
                             Button {
                             Button {
                                 UIApplication.shared.open(appURL, options: [:]) { success in
                                 UIApplication.shared.open(appURL, options: [:]) { success in
@@ -61,7 +114,8 @@ extension CGM {
                             }
                             }
 
 
                             label: {
                             label: {
-                                Label(state.displayNameOfApp() ?? "-", systemImage: "waveform.path.ecg.rectangle").font(.title3) }
+                                Label(state.displayNameOfApp() ?? "-", systemImage: "waveform.path.ecg.rectangle").font(.title3)
+                                    .padding() }
                                 .frame(maxWidth: .infinity, alignment: .center)
                                 .frame(maxWidth: .infinity, alignment: .center)
                                 .buttonStyle(.bordered)
                                 .buttonStyle(.bordered)
                         }
                         }
@@ -77,7 +131,7 @@ extension CGM {
                                         }
                                         }
                                     }
                                     }
                                 }
                                 }
-                                label: { Label("Open URL", systemImage: "waveform.path.ecg.rectangle").font(.title3) }
+                                label: { Label("Open URL", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding() }
                                     .frame(maxWidth: .infinity, alignment: .center)
                                     .frame(maxWidth: .infinity, alignment: .center)
                                     .buttonStyle(.bordered)
                                     .buttonStyle(.bordered)
                             }
                             }
@@ -86,9 +140,9 @@ extension CGM {
                             Section {
                             Section {
                                 Button {
                                 Button {
                                     state.showModal(for: .nighscoutConfigDirect)
                                     state.showModal(for: .nighscoutConfigDirect)
-                                    // router.mainSecondaryModalView.send(router.view(for: .nighscoutConfigDirect))
                                 }
                                 }
-                                label: { Label("Config Nightscout", systemImage: "waveform.path.ecg.rectangle").font(.title3)
+                                label: {
+                                    Label("Config Nightscout", systemImage: "waveform.path.ecg.rectangle").font(.title3).padding()
                                 }
                                 }
                                 .frame(maxWidth: .infinity, alignment: .center)
                                 .frame(maxWidth: .infinity, alignment: .center)
                                 .buttonStyle(.bordered)
                                 .buttonStyle(.bordered)
@@ -97,68 +151,78 @@ extension CGM {
                         }
                         }
                     }
                     }
 
 
-                    if state.cgmCurrent.type == .plugin {
-                        Section {
-                            Button("CGM Configuration") {
-                                setupCGM.toggle()
-                            }
-                        }
-                    }
                     if state.cgmCurrent.type == .xdrip {
                     if state.cgmCurrent.type == .xdrip {
                         Section(header: Text("Heartbeat")) {
                         Section(header: Text("Heartbeat")) {
                             VStack(alignment: .leading) {
                             VStack(alignment: .leading) {
                                 if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
                                 if let cgmTransmitterDeviceAddress = state.cgmTransmitterDeviceAddress {
-                                    Text("CGM address :")
+                                    Text("CGM address :").padding(.top)
                                     Text(cgmTransmitterDeviceAddress)
                                     Text(cgmTransmitterDeviceAddress)
                                 } else {
                                 } else {
-                                    Text("CGM is not used as heartbeat.")
+                                    Text("CGM is not used as heartbeat.").padding(.top)
                                 }
                                 }
+
+                                HStack(alignment: .top) {
+                                    Text(
+                                        "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                    )
+                                    .font(.footnote)
+                                    .foregroundColor(.secondary)
+                                    .lineLimit(nil)
+                                    Spacer()
+                                    Button(
+                                        action: {
+                                            hintLabel = "CGM Heartbeat"
+                                            selectedVerboseHint = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr."
+                                            shouldDisplayHint.toggle()
+                                        },
+                                        label: {
+                                            HStack {
+                                                Image(systemName: "questionmark.circle")
+                                            }
+                                        }
+                                    ).buttonStyle(BorderlessButtonStyle())
+                                }.padding(.vertical)
                             }
                             }
-                        }
+                        }.listRowBackground(Color.chart)
                     }
                     }
+
                     if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
                     if state.cgmCurrent.type == .plugin && state.cgmCurrent.id.contains("Libre") {
-                        Section(header: Text("Calibrations")) {
-                            Text("Calibrations").navigationLink(to: .calibrations, from: self)
-                        }
+                        Section {
+                            Text("Libre Calibrations").navigationLink(to: .calibrations, from: self)
+                        }.listRowBackground(Color.chart)
                     }
                     }
 
 
-                    // }
-
-                    Section(header: Text("Calendar")) {
-                        Toggle("Create Events in Calendar", isOn: $state.createCalendarEvents)
-                        if state.calendarIDs.isNotEmpty {
-                            Picker("Calendar", selection: $state.currentCalendarID) {
-                                ForEach(state.calendarIDs, id: \.self) {
-                                    Text($0).tag($0)
-                                }
-                            }
-                            Toggle("Display Emojis as Labels", isOn: $state.displayCalendarEmojis)
-                            Toggle("Display IOB and COB", isOn: $state.displayCalendarIOBandCOB)
-                        } else if state.createCalendarEvents {
-                            if #available(iOS 17.0, *) {
-                                Text(
-                                    "If you are not seeing calendars to choose here, please go to Settings -> iAPS -> Calendars and change permissions to \"Full Access\""
-                                ).font(.footnote)
-
-                                Button("Open Settings") {
-                                    // Get the settings URL and open it
-                                    if let url = URL(string: UIApplication.openSettingsURLString) {
-                                        UIApplication.shared.open(url)
-                                    }
-                                }
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.smoothGlucose,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Smooth Glucose Value"
                             }
                             }
-                        }
-                    }
-
-                    Section(header: Text("Experimental")) {
-                        Toggle("Smooth Glucose Value", isOn: $state.smoothGlucose)
-                    }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Smooth Glucose Value",
+                        miniHint: "Smooth CGM readings using Savitzky–Golay filtering.",
+                        verboseHint: "Smooth Glucose Value… bla bla bla"
+                    )
                 }
                 }
                 .scrollContentBackground(.hidden).background(color)
                 .scrollContentBackground(.hidden).background(color)
                 .onAppear(perform: configureView)
                 .onAppear(perform: configureView)
                 .navigationTitle("CGM")
                 .navigationTitle("CGM")
                 .navigationBarTitleDisplayMode(.automatic)
                 .navigationBarTitleDisplayMode(.automatic)
-                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
+                .sheet(isPresented: $shouldDisplayHint) {
+                    SettingInputHintView(
+                        hintDetent: $hintDetent,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        hintLabel: hintLabel ?? "",
+                        hintText: selectedVerboseHint ?? "",
+                        sheetTitle: "Help"
+                    )
+                }
                 .sheet(isPresented: $setupCGM) {
                 .sheet(isPresented: $setupCGM) {
                     if let cgmFetchManager = state.cgmManager,
                     if let cgmFetchManager = state.cgmManager,
                        let cgmManager = cgmFetchManager.cgmManager,
                        let cgmManager = cgmFetchManager.cgmManager,

+ 5 - 0
FreeAPS/Sources/Modules/CalendarEventSettings/CalendarEventSettingsDataFlow.swift

@@ -0,0 +1,5 @@
+enum CalendarEventSettings {
+    enum Config {}
+}
+
+protocol CalendarEventSettingsProvider: Provider {}

+ 3 - 0
FreeAPS/Sources/Modules/CalendarEventSettings/CalendarEventSettingsProvider.swift

@@ -0,0 +1,3 @@
+extension CalendarEventSettings {
+    final class Provider: BaseProvider, CalendarEventSettingsProvider {}
+}

+ 65 - 0
FreeAPS/Sources/Modules/CalendarEventSettings/CalendarEventSettingsStateModel.swift

@@ -0,0 +1,65 @@
+import Combine
+import SwiftUI
+
+extension CalendarEventSettings {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Injected() var storage: FileStorage!
+        @Injected() var calendarManager: CalendarManager!
+
+        @Published var units: GlucoseUnits = .mgdL
+        @Published var useCalendar = false
+        @Published var displayCalendarIOBandCOB = false
+        @Published var displayCalendarEmojis = false
+        @Published var calendarIDs: [String] = []
+        @Published var currentCalendarID: String = ""
+        @Persisted(key: "CalendarManager.currentCalendarID") var storedCalendarID: String? = nil
+
+        override func subscribe() {
+            units = settingsManager.settings.units
+
+            currentCalendarID = storedCalendarID ?? ""
+            calendarIDs = calendarManager.calendarIDs()
+
+            subscribeSetting(\.useCalendar, on: $useCalendar) { useCalendar = $0 }
+            subscribeSetting(\.displayCalendarIOBandCOB, on: $displayCalendarIOBandCOB) { displayCalendarIOBandCOB = $0 }
+            subscribeSetting(\.displayCalendarEmojis, on: $displayCalendarEmojis) { displayCalendarEmojis = $0 }
+
+            observeCreateCalendarEvents()
+            observeCurrentCalendarID()
+        }
+
+        private func observeCreateCalendarEvents() {
+            Task {
+                for await ok in $useCalendar.removeDuplicates().values {
+                    guard ok else { continue }
+                    let accessGranted = await calendarManager.requestAccessIfNeeded()
+                    if accessGranted {
+                        let ids = calendarManager.calendarIDs()
+                        await MainActor.run {
+                            self.calendarIDs = ids
+                        }
+                    }
+                }
+            }
+        }
+
+        private func observeCurrentCalendarID() {
+            Task {
+                for await id in $currentCalendarID.removeDuplicates().values {
+                    if id.isEmpty {
+                        calendarManager.currentCalendarID = nil
+                    } else {
+                        calendarManager.currentCalendarID = id
+                    }
+                }
+            }
+        }
+    }
+}
+
+extension CalendarEventSettings.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 132 - 0
FreeAPS/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift

@@ -0,0 +1,132 @@
+import SwiftUI
+import Swinject
+
+extension CalendarEventSettings {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @EnvironmentObject var appIcons: Icons
+
+        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
+                )
+        }
+
+        var body: some View {
+            List {
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.useCalendar,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = "Create Events in Calendar"
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: "Create Events in Calendar",
+                    miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                    verboseHint: "Create Calendar Events… bla bla bla",
+                    headerText: "Diabetes Data as Calendar Event"
+                )
+
+                if state.calendarIDs.isNotEmpty, state.useCalendar {
+                    Section {
+                        VStack {
+                            Picker("Choose Calendar", selection: $state.currentCalendarID) {
+                                ForEach(state.calendarIDs, id: \.self) {
+                                    Text($0).tag($0)
+                                }
+                            }
+                        }
+                    }.listRowBackground(Color.chart)
+
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.displayCalendarEmojis,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Display Emojis as Labels"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Display Emojis as Labels",
+                        miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                        verboseHint: "Display Emojis as Labels… bla bla bla"
+                    )
+
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.displayCalendarIOBandCOB,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Display IOB and COB"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Display IOB and COB",
+                        miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                        verboseHint: "Display IOB and COB… bla bla bla"
+                    )
+                } else if state.useCalendar {
+                    if #available(iOS 17.0, *) {
+                        Text(
+                            "If you are not seeing calendars to choose here, please go to Settings -> Trio -> Calendars and change permissions to \"Full Access\""
+                        ).font(.footnote)
+
+                        Button("Open Settings") {
+                            // Get the settings URL and open it
+                            if let url = URL(string: UIApplication.openSettingsURLString) {
+                                UIApplication.shared.open(url)
+                            }
+                        }
+                    }
+                }
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationTitle("Calendar Events")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 4 - 4
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -53,7 +53,7 @@ extension Calibrations {
                         }
                         }
                         label: { Text("Add") }
                         label: { Text("Add") }
                             .disabled(state.newCalibration <= 0)
                             .disabled(state.newCalibration <= 0)
-                    }
+                    }.listRowBackground(Color.chart)
 
 
                     Section(header: Text("Info")) {
                     Section(header: Text("Info")) {
                         HStack {
                         HStack {
@@ -66,7 +66,7 @@ extension Calibrations {
                             Spacer()
                             Spacer()
                             Text(formatter.string(from: state.intercept as NSNumber)!)
                             Text(formatter.string(from: state.intercept as NSNumber)!)
                         }
                         }
-                    }
+                    }.listRowBackground(Color.chart)
 
 
                     Section(header: Text("Remove")) {
                     Section(header: Text("Remove")) {
                         Button {
                         Button {
@@ -97,13 +97,13 @@ extension Calibrations {
 
 
                             }.onDelete(perform: delete)
                             }.onDelete(perform: delete)
                         }
                         }
-                    }
+                    }.listRowBackground(Color.chart)
 
 
                     if state.calibrations.isNotEmpty {
                     if state.calibrations.isNotEmpty {
                         Section(header: Text("Chart")) {
                         Section(header: Text("Chart")) {
                             CalibrationsChart().environmentObject(state)
                             CalibrationsChart().environmentObject(state)
                                 .frame(minHeight: geo.size.width)
                                 .frame(minHeight: geo.size.width)
-                        }
+                        }.listRowBackground(Color.chart)
                     }
                     }
                 }
                 }
             }
             }

+ 4 - 3
FreeAPS/Sources/Modules/CREditor/CREditorDataFlow.swift

@@ -1,6 +1,6 @@
 import Foundation
 import Foundation
 
 
-enum CREditor {
+enum CarbRatioEditor {
     enum Config {}
     enum Config {}
 
 
     class Item: Identifiable, Hashable, Equatable {
     class Item: Identifiable, Hashable, Equatable {
@@ -14,16 +14,17 @@ enum CREditor {
         }
         }
 
 
         static func == (lhs: Item, rhs: Item) -> Bool {
         static func == (lhs: Item, rhs: Item) -> Bool {
-            lhs.timeIndex == rhs.timeIndex
+            lhs.timeIndex == rhs.timeIndex && lhs.rateIndex == rhs.rateIndex
         }
         }
 
 
         func hash(into hasher: inout Hasher) {
         func hash(into hasher: inout Hasher) {
             hasher.combine(timeIndex)
             hasher.combine(timeIndex)
+            hasher.combine(rateIndex)
         }
         }
     }
     }
 }
 }
 
 
-protocol CREditorProvider: Provider {
+protocol CarbRatioEditorProvider: Provider {
     var profile: CarbRatios { get }
     var profile: CarbRatios { get }
     func saveProfile(_ profile: CarbRatios)
     func saveProfile(_ profile: CarbRatios)
     var autotune: Autotune? { get }
     var autotune: Autotune? { get }

+ 2 - 2
FreeAPS/Sources/Modules/CREditor/CREditorProvider.swift

@@ -1,7 +1,7 @@
 import Combine
 import Combine
 
 
-extension CREditor {
-    final class Provider: BaseProvider, CREditorProvider {
+extension CarbRatioEditor {
+    final class Provider: BaseProvider, CarbRatioEditorProvider {
         var profile: CarbRatios {
         var profile: CarbRatios {
             storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
             storage.retrieve(OpenAPS.Settings.carbRatios, as: CarbRatios.self)
                 ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))
                 ?? CarbRatios(from: OpenAPS.defaults(for: OpenAPS.Settings.carbRatios))

+ 31 - 2
FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift

@@ -1,9 +1,12 @@
 import SwiftUI
 import SwiftUI
 
 
-extension CREditor {
+extension CarbRatioEditor {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
+        @Injected() private var nightscout: NightscoutManager!
         @Published var items: [Item] = []
         @Published var items: [Item] = []
+        @Published var initialItems: [Item] = []
         @Published var autotune: Autotune?
         @Published var autotune: Autotune?
+        @Published var shouldDisplaySaving: Bool = false
 
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
 
@@ -14,6 +17,20 @@ extension CREditor {
             return lastItem.timeIndex < timeValues.count - 1
             return lastItem.timeIndex < timeValues.count - 1
         }
         }
 
 
+        var hasChanges: Bool {
+            if initialItems.count != items.count {
+                return true
+            }
+
+            for (initialItem, currentItem) in zip(initialItems, items) {
+                if initialItem.rateIndex != currentItem.rateIndex || initialItem.timeIndex != currentItem.timeIndex {
+                    return true
+                }
+            }
+
+            return false
+        }
+
         override func subscribe() {
         override func subscribe() {
             items = provider.profile.schedule.map { value in
             items = provider.profile.schedule.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
                 let timeIndex = timeValues.firstIndex(of: Double(value.offset * 60)) ?? 0
@@ -21,6 +38,8 @@ extension CREditor {
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
                 return Item(rateIndex: rateIndex, timeIndex: timeIndex)
             }
             }
 
 
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+
             autotune = provider.autotune
             autotune = provider.autotune
         }
         }
 
 
@@ -38,6 +57,9 @@ extension CREditor {
         }
         }
 
 
         func save() {
         func save() {
+            guard hasChanges else { return }
+            shouldDisplaySaving = true
+
             let schedule = items.enumerated().map { _, item -> CarbRatioEntry in
             let schedule = items.enumerated().map { _, item -> CarbRatioEntry in
                 let fotmatter = DateFormatter()
                 let fotmatter = DateFormatter()
                 fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
                 fotmatter.timeZone = TimeZone(secondsFromGMT: 0)
@@ -49,6 +71,11 @@ extension CREditor {
             }
             }
             let profile = CarbRatios(units: .grams, schedule: schedule)
             let profile = CarbRatios(units: .grams, schedule: schedule)
             provider.saveProfile(profile)
             provider.saveProfile(profile)
+            initialItems = items.map { Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
+            Task.detached(priority: .low) {
+                debug(.nightscout, "Attempting to upload CRs to Nightscout")
+                await self.nightscout.uploadProfiles()
+            }
         }
         }
 
 
         func validate() {
         func validate() {
@@ -56,7 +83,9 @@ extension CREditor {
                 let uniq = Array(Set(self.items))
                 let uniq = Array(Set(self.items))
                 let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
                 let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
                 sorted.first?.timeIndex = 0
                 sorted.first?.timeIndex = 0
-                self.items = sorted
+                if self.items != sorted {
+                    self.items = sorted
+                }
             }
             }
         }
         }
     }
     }

+ 66 - 49
FreeAPS/Sources/Modules/CREditor/View/CREditorRootView.swift

@@ -1,7 +1,7 @@
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
-extension CREditor {
+extension CarbRatioEditor {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
@@ -40,6 +40,8 @@ extension CREditor {
 
 
         var body: some View {
         var body: some View {
             Form {
             Form {
+                let shouldDisableButton = state.shouldDisplaySaving || state.items.isEmpty || !state.hasChanges
+
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                 if let autotune = state.autotune, !state.settingsManager.settings.onlyAutotuneBasals {
                     Section(header: Text("Autotune")) {
                     Section(header: Text("Autotune")) {
                         HStack {
                         HStack {
@@ -48,31 +50,49 @@ extension CREditor {
                             Text(rateFormatter.string(from: autotune.carbRatio as NSNumber) ?? "0")
                             Text(rateFormatter.string(from: autotune.carbRatio as NSNumber) ?? "0")
                             Text("g/U").foregroundColor(.secondary)
                             Text("g/U").foregroundColor(.secondary)
                         }
                         }
-                    }
+                    }.listRowBackground(Color.chart)
                 }
                 }
+
                 Section(header: Text("Schedule")) {
                 Section(header: Text("Schedule")) {
                     list
                     list
-                    addButton
-                }
+                }.listRowBackground(Color.chart)
+
                 Section {
                 Section {
-                    Button {
-                        let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                        impactHeavy.impactOccurred()
-                        state.save()
-                    }
-                    label: {
-                        Text("Save")
+                    HStack {
+                        if state.shouldDisplaySaving {
+                            ProgressView().padding(.trailing, 10)
+                        }
+
+                        Button {
+                            let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                            impactHeavy.impactOccurred()
+                            state.save()
+
+                            // deactivate saving display after 1.25 seconds
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 1.25) {
+                                state.shouldDisplaySaving = false
+                            }
+                        } label: {
+                            Text(state.shouldDisplaySaving ? "Saving..." : "Save")
+                        }
+                        .disabled(shouldDisableButton)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
                     }
                     }
-                    .disabled(state.items.isEmpty)
-                }
+                }.listRowBackground(shouldDisableButton ? Color(.systemGray4) : Color(.systemBlue))
             }
             }
             .scrollContentBackground(.hidden).background(color)
             .scrollContentBackground(.hidden).background(color)
             .onAppear(perform: configureView)
             .onAppear(perform: configureView)
             .navigationTitle("Carb Ratios")
             .navigationTitle("Carb Ratios")
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarTitleDisplayMode(.automatic)
-            .navigationBarItems(
-                trailing: EditButton()
-            )
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarTrailing) {
+                    EditButton()
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    addButton
+                }
+            })
             .environment(\.editMode, $editMode)
             .environment(\.editMode, $editMode)
             .onAppear {
             .onAppear {
                 state.validate()
                 state.validate()
@@ -80,42 +100,39 @@ extension CREditor {
         }
         }
 
 
         private func pickers(for index: Int) -> some View {
         private func pickers(for index: Int) -> some View {
-            GeometryReader { geometry in
-                VStack {
-                    HStack {
-                        Text("Ratio").frame(width: geometry.size.width / 2)
-                        Text("Time").frame(width: geometry.size.width / 2)
-                    }
-                    HStack(spacing: 0) {
-                        Picker(selection: $state.items[index].rateIndex, label: EmptyView()) {
-                            ForEach(0 ..< state.rateValues.count, id: \.self) { i in
-                                Text(
-                                    (
-                                        self.rateFormatter
-                                            .string(from: state.rateValues[i] as NSNumber) ?? ""
-                                    ) + " g/U"
-                                ).tag(i)
-                            }
+            Form {
+                Section {
+                    Picker(selection: $state.items[index].rateIndex, label: Text("Ratio")) {
+                        ForEach(0 ..< state.rateValues.count, id: \.self) { i in
+                            Text(
+                                (
+                                    self.rateFormatter
+                                        .string(from: state.rateValues[i] as NSNumber) ?? ""
+                                ) + " g/U"
+                            ).tag(i)
                         }
                         }
-                        .frame(maxWidth: geometry.size.width / 2)
-                        .clipped()
-
-                        Picker(selection: $state.items[index].timeIndex, label: EmptyView()) {
-                            ForEach(0 ..< state.timeValues.count, id: \.self) { i in
-                                Text(
-                                    self.dateFormatter
-                                        .string(from: Date(
-                                            timeIntervalSince1970: state
-                                                .timeValues[i]
-                                        ))
-                                ).tag(i)
-                            }
+                    }
+                }.listRowBackground(Color.chart)
+
+                Section {
+                    Picker(selection: $state.items[index].timeIndex, label: Text("Time")) {
+                        ForEach(0 ..< state.timeValues.count, id: \.self) { i in
+                            Text(
+                                self.dateFormatter
+                                    .string(from: Date(
+                                        timeIntervalSince1970: state
+                                            .timeValues[i]
+                                    ))
+                            ).tag(i)
                         }
                         }
-                        .frame(maxWidth: geometry.size.width / 2)
-                        .clipped()
                     }
                     }
-                }
+
+                }.listRowBackground(Color.chart)
             }
             }
+            .padding(.top)
+            .scrollContentBackground(.hidden).background(color)
+            .navigationTitle("Set Ratio")
+            .navigationBarTitleDisplayMode(.automatic)
         }
         }
 
 
         private var list: some View {
         private var list: some View {
@@ -147,7 +164,7 @@ extension CREditor {
 
 
             switch editMode {
             switch editMode {
             case .inactive:
             case .inactive:
-                return AnyView(Button(action: onAdd) { Text("Add") })
+                return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
             default:
             default:
                 return AnyView(EmptyView())
                 return AnyView(EmptyView())
             }
             }

+ 8 - 25
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -10,6 +10,7 @@ extension DataTable {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var healthKitManager: HealthKitManager!
         @Injected() var healthKitManager: HealthKitManager!
+        @Injected() var carbsStorage: CarbsStorage!
 
 
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let coredataContext = CoreDataStack.shared.newTaskContext()
 
 
@@ -97,6 +98,7 @@ extension DataTable {
 
 
             var carbEntry: CarbEntryStored?
             var carbEntry: CarbEntryStored?
 
 
+            // Delete carbs or FPUs from Nightscout
             await taskContext.perform {
             await taskContext.perform {
                 do {
                 do {
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
                     carbEntry = try taskContext.existingObject(with: treatmentObjectID) as? CarbEntryStored
@@ -108,42 +110,23 @@ extension DataTable {
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                     if carbEntry.isFPU, let fpuID = carbEntry.fpuID {
                         // Delete FPUs from Nightscout
                         // Delete FPUs from Nightscout
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
                         self.provider.deleteCarbsFromNightscout(withID: fpuID.uuidString)
-
-                        // fetch request for all carb entries with the same id
-                        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CarbEntryStored.fetchRequest()
-                        fetchRequest.predicate = NSPredicate(format: "fpuID == %@", fpuID as CVarArg)
-
-                        // NSBatchDeleteRequest
-                        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
-                        deleteRequest.resultType = .resultTypeCount
-
-                        // execute the batch delete request
-                        let result = try taskContext.execute(deleteRequest) as? NSBatchDeleteResult
-                        debugPrint("\(DebuggingIdentifiers.succeeded) Deleted \(result?.result ?? 0) items with FpuID \(fpuID)")
-
-                        Foundation.NotificationCenter.default.post(name: .didPerformBatchDelete, object: nil)
                     } else {
                     } else {
                         // Delete carbs from Nightscout
                         // Delete carbs from Nightscout
                         if let id = carbEntry.id?.uuidString {
                         if let id = carbEntry.id?.uuidString {
                             self.provider.deleteCarbsFromNightscout(withID: id)
                             self.provider.deleteCarbsFromNightscout(withID: id)
                         }
                         }
-
-                        // Now delete carbs also from the Database
-                        taskContext.delete(carbEntry)
-
-                        guard taskContext.hasChanges else { return }
-                        try taskContext.save()
-
-                        debugPrint(
-                            "Data Table State: \(#function) \(DebuggingIdentifiers.succeeded) deleted carb entry from core data"
-                        )
                     }
                     }
 
 
                 } catch {
                 } catch {
-                    debugPrint("\(DebuggingIdentifiers.failed) Error deleting carb entry: \(error.localizedDescription)")
+                    debugPrint(
+                        "\(DebuggingIdentifiers.failed) Error deleting carb entry from Nightscout: \(error.localizedDescription)"
+                    )
                 }
                 }
             }
             }
 
 
+            // Delete carbs from Core Data
+            await carbsStorage.deleteCarbs(treatmentObjectID)
+
             // Perform a determine basal sync to update cob
             // Perform a determine basal sync to update cob
             await apsManager.determineBasalSync()
             await apsManager.determineBasalSync()
         }
         }

+ 15 - 3
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -70,7 +70,7 @@ extension DataTable {
                 formatter.minimumFractionDigits = 0
                 formatter.minimumFractionDigits = 0
                 formatter.maximumFractionDigits = 1
                 formatter.maximumFractionDigits = 1
             }
             }
-            formatter.roundingMode = .down
+            formatter.roundingMode = .halfUp
             return formatter
             return formatter
         }
         }
 
 
@@ -143,12 +143,24 @@ extension DataTable {
                 .navigationTitle("History")
                 .navigationTitle("History")
                 .navigationBarTitleDisplayMode(.large)
                 .navigationBarTitleDisplayMode(.large)
                 .toolbar {
                 .toolbar {
-                    ToolbarItem(placement: .topBarTrailing) {
+                    ToolbarItem(placement: .topBarLeading, content: {
+                        Button(
+                            action: { state.showModal(for: .statistics) },
+                            label: {
+                                HStack {
+                                    Text("Statistics")
+                                }
+                            }
+                        )
+                    })
+                }
+                .toolbar {
+                    ToolbarItem(placement: .topBarTrailing, content: {
                         addButton({
                         addButton({
                             showManualGlucose = true
                             showManualGlucose = true
                             state.manualGlucose = 0
                             state.manualGlucose = 0
                         })
                         })
-                    }
+                    })
                 }
                 }
                 .sheet(isPresented: $showManualGlucose) {
                 .sheet(isPresented: $showManualGlucose) {
                     addGlucoseView()
                     addGlucoseView()

+ 0 - 5
FreeAPS/Sources/Modules/Dynamic/DynamicDataFlow.swift

@@ -1,5 +0,0 @@
-enum Dynamic {
-    enum Config {}
-}
-
-protocol DynamicProvider: Provider {}

+ 0 - 3
FreeAPS/Sources/Modules/Dynamic/DynamicProvider.swift

@@ -1,3 +0,0 @@
-extension Dynamic {
-    final class Provider: BaseProvider, DynamicProvider {}
-}

+ 0 - 112
FreeAPS/Sources/Modules/Dynamic/View/DynamicRootView.swift

@@ -1,112 +0,0 @@
-import SwiftUI
-import Swinject
-
-extension Dynamic {
-    struct RootView: BaseView {
-        let resolver: Resolver
-        @StateObject var state = StateModel()
-
-        private var conversionFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 1
-
-            return formatter
-        }
-
-        @Environment(\.colorScheme) var colorScheme
-        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 var formatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            return formatter
-        }
-
-        private var glucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            if state.unit == .mmolL {
-                formatter.maximumFractionDigits = 1
-            } else { formatter.maximumFractionDigits = 0 }
-            formatter.roundingMode = .halfUp
-            return formatter
-        }
-
-        var body: some View {
-            Form {
-                Section {
-                    HStack {
-                        Toggle("Activate Dynamic Sensitivity (ISF)", isOn: $state.useNewFormula)
-                    }
-                    if state.useNewFormula {
-                        HStack {
-                            Toggle("Activate Dynamic Carb Ratio (CR)", isOn: $state.enableDynamicCR)
-                        }
-                    }
-                } header: { Text("Enable") }
-
-                if state.useNewFormula {
-                    Section {
-                        HStack {
-                            Toggle("Use Sigmoid Formula", isOn: $state.sigmoid)
-                        }
-                    } header: { Text("Formula") }
-
-                    Section {
-                        HStack {
-                            Text("Adjustment Factor")
-                            Spacer()
-                            TextFieldWithToolBar(text: $state.adjustmentFactor, placeholder: "0", numberFormatter: formatter)
-                        }
-
-                        HStack {
-                            Text("Weighted Average of TDD. Weight of past 24 hours:")
-                            Spacer()
-                            TextFieldWithToolBar(text: $state.weightPercentage, placeholder: "0", numberFormatter: formatter)
-                        }
-
-                        HStack {
-                            Toggle("Adjust basal", isOn: $state.tddAdjBasal)
-                        }
-                    } header: { Text("Settings") }
-
-                    Section {
-                        HStack {
-                            Text("Threshold Setting")
-                            Spacer()
-                            TextFieldWithToolBar(
-                                text: $state.threshold_setting,
-                                placeholder: "0",
-                                numberFormatter: glucoseFormatter
-                            )
-                            Text(state.unit.rawValue)
-                        }
-                    } header: { Text("Safety") }
-                }
-            }
-            .scrollContentBackground(.hidden).background(color)
-            .onAppear(perform: configureView)
-            .navigationBarTitle("Dynamic ISF")
-            .navigationBarTitleDisplayMode(.automatic)
-            .onDisappear {
-                state.saveIfChanged()
-            }
-        }
-    }
-}

+ 5 - 0
FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsDataFlow.swift

@@ -0,0 +1,5 @@
+enum DynamicSettings {
+    enum Config {}
+}
+
+protocol DynamicSettingsProvider: Provider {}

+ 3 - 0
FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsProvider.swift

@@ -0,0 +1,3 @@
+extension DynamicSettings {
+    final class Provider: BaseProvider, DynamicSettingsProvider {}
+}

+ 18 - 20
FreeAPS/Sources/Modules/Dynamic/DynamicStateModel.swift

@@ -1,6 +1,6 @@
 import SwiftUI
 import SwiftUI
 
 
-extension Dynamic {
+extension DynamicSettings {
     final class StateModel: BaseStateModel<Provider> {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() var settings: SettingsManager!
         @Injected() var settings: SettingsManager!
         @Injected() var storage: FileStorage!
         @Injected() var storage: FileStorage!
@@ -8,57 +8,49 @@ extension Dynamic {
         @Published var useNewFormula: Bool = false
         @Published var useNewFormula: Bool = false
         @Published var enableDynamicCR: Bool = false
         @Published var enableDynamicCR: Bool = false
         @Published var sigmoid: Bool = false
         @Published var sigmoid: Bool = false
-        @Published var adjustmentFactor: Decimal = 0.5
+        @Published var adjustmentFactor: Decimal = 0.8
+        @Published var adjustmentFactorSigmoid: Decimal = 0.5
         @Published var weightPercentage: Decimal = 0.65
         @Published var weightPercentage: Decimal = 0.65
         @Published var tddAdjBasal: Bool = false
         @Published var tddAdjBasal: Bool = false
-        @Published var threshold_setting: Decimal = 65
-        @Published var unit: GlucoseUnits = .mgdL
+        @Published var threshold_setting: Decimal = 60
+        @Published var units: GlucoseUnits = .mgdL
 
 
         var preferences: Preferences {
         var preferences: Preferences {
             settingsManager.preferences
             settingsManager.preferences
         }
         }
 
 
         override func subscribe() {
         override func subscribe() {
-            unit = settingsManager.settings.units
+            units = settingsManager.settings.units
             useNewFormula = settings.preferences.useNewFormula
             useNewFormula = settings.preferences.useNewFormula
             enableDynamicCR = settings.preferences.enableDynamicCR
             enableDynamicCR = settings.preferences.enableDynamicCR
             sigmoid = settings.preferences.sigmoid
             sigmoid = settings.preferences.sigmoid
             adjustmentFactor = settings.preferences.adjustmentFactor
             adjustmentFactor = settings.preferences.adjustmentFactor
+            adjustmentFactorSigmoid = settings.preferences.adjustmentFactorSigmoid
             weightPercentage = settings.preferences.weightPercentage
             weightPercentage = settings.preferences.weightPercentage
             tddAdjBasal = settings.preferences.tddAdjBasal
             tddAdjBasal = settings.preferences.tddAdjBasal
-
-            if unit == .mmolL {
-                threshold_setting = settings.preferences.threshold_setting.asMmolL
-            } else {
-                threshold_setting = settings.preferences.threshold_setting
-            }
+            threshold_setting = settings.preferences.threshold_setting
         }
         }
 
 
         var unChanged: Bool {
         var unChanged: Bool {
             preferences.enableDynamicCR == enableDynamicCR &&
             preferences.enableDynamicCR == enableDynamicCR &&
                 preferences.adjustmentFactor == adjustmentFactor &&
                 preferences.adjustmentFactor == adjustmentFactor &&
                 preferences.sigmoid == sigmoid &&
                 preferences.sigmoid == sigmoid &&
+                preferences.adjustmentFactorSigmoid == adjustmentFactorSigmoid &&
                 preferences.tddAdjBasal == tddAdjBasal &&
                 preferences.tddAdjBasal == tddAdjBasal &&
-                preferences.threshold_setting == convertBack(threshold_setting) &&
+                preferences.threshold_setting == threshold_setting &&
                 preferences.useNewFormula == useNewFormula &&
                 preferences.useNewFormula == useNewFormula &&
                 preferences.weightPercentage == weightPercentage
                 preferences.weightPercentage == weightPercentage
         }
         }
 
 
-        func convertBack(_ glucose: Decimal) -> Decimal {
-            if unit == .mmolL {
-                return glucose.asMgdL
-            }
-            return glucose
-        }
-
         func saveIfChanged() {
         func saveIfChanged() {
             if !unChanged {
             if !unChanged {
                 var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
                 var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
                 newSettings.enableDynamicCR = enableDynamicCR
                 newSettings.enableDynamicCR = enableDynamicCR
                 newSettings.adjustmentFactor = adjustmentFactor
                 newSettings.adjustmentFactor = adjustmentFactor
                 newSettings.sigmoid = sigmoid
                 newSettings.sigmoid = sigmoid
+                newSettings.adjustmentFactorSigmoid = adjustmentFactorSigmoid
                 newSettings.tddAdjBasal = tddAdjBasal
                 newSettings.tddAdjBasal = tddAdjBasal
-                newSettings.threshold_setting = convertBack(threshold_setting)
+                newSettings.threshold_setting = threshold_setting
                 newSettings.useNewFormula = useNewFormula
                 newSettings.useNewFormula = useNewFormula
                 newSettings.weightPercentage = weightPercentage
                 newSettings.weightPercentage = weightPercentage
                 newSettings.timestamp = Date()
                 newSettings.timestamp = Date()
@@ -67,3 +59,9 @@ extension Dynamic {
         }
         }
     }
     }
 }
 }
+
+extension DynamicSettings.StateModel: SettingsObserver {
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
+}

+ 226 - 0
FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -0,0 +1,226 @@
+import SwiftUI
+import Swinject
+
+extension DynamicSettings {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+        @State private var shouldDisplayHint: Bool = false
+        @State var hintDetent = PresentationDetent.large
+        @State var selectedVerboseHint: String?
+        @State var hintLabel: String?
+        @State private var decimalPlaceholder: Decimal = 0.0
+        @State private var booleanPlaceholder: Bool = false
+
+        private var conversionFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+
+            return formatter
+        }
+
+        @Environment(\.colorScheme) var colorScheme
+        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 var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            return formatter
+        }
+
+        private var glucoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            if state.units == .mmolL {
+                formatter.maximumFractionDigits = 1
+            } else { formatter.maximumFractionDigits = 0 }
+            formatter.roundingMode = .halfUp
+            return formatter
+        }
+
+        var body: some View {
+            List {
+                SettingInputSection(
+                    decimalValue: $decimalPlaceholder,
+                    booleanValue: $state.useNewFormula,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    selectedVerboseHint: Binding(
+                        get: { selectedVerboseHint },
+                        set: {
+                            selectedVerboseHint = $0
+                            hintLabel = "Activate Dynamic Sensitivity (ISF)"
+                        }
+                    ),
+                    units: state.units,
+                    type: .boolean,
+                    label: "Activate Dynamic Sensitivity (ISF)",
+                    miniHint: "Trio calculates insulin sensitivity (ISF) each loop cycle based on current blood sugar, daily insulin use, and an adjustment factor, within set limits.",
+                    verboseHint: "DynamicISF",
+                    headerText: "Dynamic Insulin Sensitivity"
+                )
+
+                if state.useNewFormula {
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.enableDynamicCR,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Activate Dynamic Carb Ratio (CR)"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Activate Dynamic Carb Ratio (CR)",
+                        miniHint: "Similar to Dynamic Sensitivity, Trio calculates a dynamic carb ratio every loop cycle.",
+                        verboseHint: "Logarithmic Dynamic Insulin Sensitivity"
+                    )
+
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.sigmoid,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Use Sigmoid Formula"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Use Sigmoid Formula",
+                        miniHint: "Alternative formula for dynamic ISF, that alters ISF based on distance from target BG",
+                        verboseHint: "Sigmoid  Dynamic Insulin Sensitivity"
+                    )
+
+                    if !state.sigmoid {
+                        SettingInputSection(
+                            decimalValue: $state.adjustmentFactor,
+                            booleanValue: $booleanPlaceholder,
+                            shouldDisplayHint: $shouldDisplayHint,
+                            selectedVerboseHint: Binding(
+                                get: { selectedVerboseHint },
+                                set: {
+                                    selectedVerboseHint = $0
+                                    hintLabel = "Adjustment Factor"
+                                }
+                            ),
+                            units: state.units,
+                            type: .decimal("adjustmentFactor"),
+                            label: "Adjustment Factor",
+                            miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                            verboseHint: "Adjustment Factor for logarithmic dynamic sensitvity... bla bla bla"
+                        )
+                    } else {
+                        SettingInputSection(
+                            decimalValue: $state.adjustmentFactorSigmoid,
+                            booleanValue: $booleanPlaceholder,
+                            shouldDisplayHint: $shouldDisplayHint,
+                            selectedVerboseHint: Binding(
+                                get: { selectedVerboseHint },
+                                set: {
+                                    selectedVerboseHint = $0
+                                    hintLabel = "Sigmoid Adjustment Factor"
+                                }
+                            ),
+                            units: state.units,
+                            type: .decimal("adjustmentFactorSigmoid"),
+                            label: "Sigmoid Adjustment Factor",
+                            miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                            verboseHint: "Sigmoid Adjustment Factor… should be 0.5… bla bla ba"
+                        )
+                    }
+
+                    SettingInputSection(
+                        decimalValue: $state.weightPercentage,
+                        booleanValue: $booleanPlaceholder,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Weighted Average of TDD"
+                            }
+                        ),
+                        units: state.units,
+                        type: .decimal("weightPercentage"),
+                        label: "Weighted Average of TDD",
+                        miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                        verboseHint: "Weight of past 24 hours"
+                    )
+
+                    SettingInputSection(
+                        decimalValue: $decimalPlaceholder,
+                        booleanValue: $state.tddAdjBasal,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Adjust Basal"
+                            }
+                        ),
+                        units: state.units,
+                        type: .boolean,
+                        label: "Adjust Basal",
+                        miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                        verboseHint: "Adjust basal dynamically… bla bla"
+                    )
+
+                    SettingInputSection(
+                        decimalValue: $state.threshold_setting,
+                        booleanValue: $booleanPlaceholder,
+                        shouldDisplayHint: $shouldDisplayHint,
+                        selectedVerboseHint: Binding(
+                            get: { selectedVerboseHint },
+                            set: {
+                                selectedVerboseHint = $0
+                                hintLabel = "Minimum Safety Threshold"
+                            }
+                        ),
+                        units: state.units,
+                        type: .decimal("threshold_setting"),
+                        label: "Minimum Safety Threshold",
+                        miniHint: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
+                        verboseHint: "Minimum Safety Threshold… bla bla bla"
+                    )
+                }
+            }
+            .sheet(isPresented: $shouldDisplayHint) {
+                SettingInputHintView(
+                    hintDetent: $hintDetent,
+                    shouldDisplayHint: $shouldDisplayHint,
+                    hintLabel: hintLabel ?? "",
+                    hintText: selectedVerboseHint ?? "",
+                    sheetTitle: "Help"
+                )
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Dynamic Settings")
+            .navigationBarTitleDisplayMode(.automatic)
+            .onDisappear {
+                state.saveIfChanged()
+            }
+        }
+    }
+}

+ 0 - 5
FreeAPS/Sources/Modules/FPUConfig/FPUConfigDataFlow.swift

@@ -1,5 +0,0 @@
-enum FPUConfig {
-    enum Config {}
-}
-
-protocol FPUConfigProvider {}

+ 0 - 0
FreeAPS/Sources/Modules/FPUConfig/FPUConfigProvider.swift


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels