Просмотр исходного кода

Merge branch 'core-data-sync-trio' into feat/move-autosens

Andreas Stokholm 1 год назад
Родитель
Сommit
d0fbed0ee1
100 измененных файлов с 6615 добавлено и 906 удалено
  1. 2 2
      Config.xcconfig
  2. 254 90
      FreeAPS.xcodeproj/project.pbxproj
  3. 3 0
      FreeAPS/Resources/Base.lproj/InfoPlist.strings
  4. 3 0
      FreeAPS/Resources/Info.plist
  5. 1 1
      FreeAPS/Resources/javascript/bundle/autosens.js
  6. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-prep.js
  7. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  8. 1 1
      FreeAPS/Resources/javascript/bundle/iob.js
  9. 1 1
      FreeAPS/Resources/javascript/bundle/meal.js
  10. 2 2
      FreeAPS/Sources/APS/APSManager.swift
  11. 87 0
      FreeAPS/Sources/APS/Extensions/FontExtensions.swift
  12. 1 1
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  13. 36 3
      FreeAPS/Sources/APS/FetchTreatmentsManager.swift
  14. 1 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  15. 112 192
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  16. 6 5
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  17. 150 0
      FreeAPS/Sources/APS/Storage/ContactTrickStorage.swift
  18. 29 3
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  19. 15 22
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  20. 1 1
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  21. 266 50
      FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift
  22. 20 0
      FreeAPS/Sources/Application/AppState.swift
  23. 21 3
      FreeAPS/Sources/Application/FreeAPSApp.swift
  24. 1 0
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  25. 1 0
      FreeAPS/Sources/Assemblies/StorageAssembly.swift
  26. 6 0
      FreeAPS/Sources/Helpers/Decimal+Extensions.swift
  27. 51 0
      FreeAPS/Sources/Helpers/Formatters.swift
  28. 138 28
      FreeAPS/Sources/Helpers/MainChartHelper.swift
  29. 1 1
      FreeAPS/Sources/Helpers/ProgressBar.swift
  30. 1 1
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  31. 1 1
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  32. 1 1
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  33. 1 1
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  34. 4 4
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  35. 1 1
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  36. 1 1
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  37. 1 1
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  38. 1 1
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  39. 1 1
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  40. 1 1
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  41. 1 1
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  42. 1 1
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  43. 1 1
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  44. 1 1
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  45. 1 1
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  46. 1 1
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  47. 1 1
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  48. 1 1
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  49. 1 1
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  50. 1 1
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  51. 1 1
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  52. 1 1
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  53. 1 1
      FreeAPS/Sources/Models/AlertEntry.swift
  54. 2 0
      FreeAPS/Sources/Models/Battery.swift
  55. 2 2
      FreeAPS/Sources/Models/CarbsEntry.swift
  56. 192 0
      FreeAPS/Sources/Models/ContactTrickEntry.swift
  57. 2 2
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  58. 1 0
      FreeAPS/Sources/Models/NightscoutStatus.swift
  59. 12 12
      FreeAPS/Sources/Models/Oref2_variables.swift
  60. 1 1
      FreeAPS/Sources/Models/Override.swift
  61. 218 1
      FreeAPS/Sources/Models/Preferences.swift
  62. 13 4
      FreeAPS/Sources/Models/TempTarget.swift
  63. 2 2
      FreeAPS/Sources/Modules/OverrideConfig/OverrideDataFlow.swift
  64. 9 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsProvider.swift
  65. 93 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift
  66. 338 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  67. 422 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  68. 272 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  69. 707 0
      FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  70. 474 0
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  71. 635 0
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  72. 388 0
      FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  73. 410 0
      FreeAPS/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  74. 19 0
      FreeAPS/Sources/Modules/Adjustments/View/ViewElements/RadioButton.swift
  75. 67 0
      FreeAPS/Sources/Modules/Adjustments/View/ViewElements/TargetPicker.swift
  76. 9 0
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsProvider.swift
  77. 32 67
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  78. 4 19
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  79. 0 1
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  80. 3 22
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  81. 2 17
      FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift
  82. 30 34
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  83. 21 42
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  84. 15 0
      FreeAPS/Sources/Modules/Base/BaseStateModel.swift
  85. 2 17
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  86. 28 42
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  87. 2 18
      FreeAPS/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift
  88. 2 17
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  89. 18 38
      FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  90. 2 17
      FreeAPS/Sources/Modules/ConfigEditor/View/ConfigEditorRootView.swift
  91. 8 0
      FreeAPS/Sources/Modules/ContactTrick/ContactTrickDataFlow.swift
  92. 6 0
      FreeAPS/Sources/Modules/ContactTrick/ContactTrickProvider.swift
  93. 179 0
      FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift
  94. 248 0
      FreeAPS/Sources/Modules/ContactTrick/View/AddContactTrickSheet.swift
  95. 216 0
      FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickDetailView.swift
  96. 89 0
      FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift
  97. 3 0
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  98. 16 29
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  99. 162 67
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  100. 0 0
      FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift

+ 2 - 2
Config.xcconfig

@@ -7,6 +7,6 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 APP_ICON = trioBlack
 APP_URL_SCHEME = Trio
 
-#include? "../../ConfigOverride.xcconfig"
+// Optional overrides
+#include? "../ConfigOverride.xcconfig"
 #include? "ConfigOverride.xcconfig"
-#include? "../../ConfigOverride.xcconfig"

+ 254 - 90
FreeAPS.xcodeproj/project.pbxproj

@@ -19,7 +19,6 @@
 		110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110AEDE92C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift */; };
 		118DF76A2C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7642C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift */; };
 		118DF76B2C5ECBC60067FEB7 /* CancelOverrideIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7652C5ECBC60067FEB7 /* CancelOverrideIntent.swift */; };
-		118DF76C2C5ECBC60067FEB7 /* ListOverridePresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7662C5ECBC60067FEB7 /* ListOverridePresetIntent.swift */; };
 		118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7672C5ECBC60067FEB7 /* OverridePresetEntity.swift */; };
 		118DF76E2C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 118DF7682C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift */; };
 		17A9D0899046B45E87834820 /* CarbRatioEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CarbRatioEditorProvider.swift */; };
@@ -247,6 +246,7 @@
 		581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A82BCEEDF800BF67D7 /* NSPredicates.swift */; };
 		581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581AC4382BE22ED10038760C /* JSONConverter.swift */; };
 		58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58237D9D2BCF0A6B00A47A79 /* PopupView.swift */; };
+		5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5825A1BD2C97335C0046467E /* EditTempTargetForm.swift */; };
 		582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF9742C8CDB92001F516D /* GlucoseChartView.swift */; };
 		582DF9772C8CDBE7001F516D /* InsulinView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF9762C8CDBE7001F516D /* InsulinView.swift */; };
 		582DF9792C8CE1E5001F516D /* MainChartHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 582DF9782C8CE1E5001F516D /* MainChartHelper.swift */; };
@@ -267,11 +267,16 @@
 		5864E8592C42CFAE00294306 /* DeterminationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5864E8582C42CFAE00294306 /* DeterminationStorage.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5887527C2BD986E1008B081D /* OpenAPSBattery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5887527B2BD986E1008B081D /* OpenAPSBattery.swift */; };
+		58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D5392C96D4DE003F90FC /* AddTempTargetForm.swift */; };
+		58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D5432C96DE11003F90FC /* TempTargetStored+Helper.swift */; };
+		58A3D5512C96EFA8003F90FC /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D54D2C96EFA8003F90FC /* TempTargetStored+CoreDataClass.swift */; };
+		58A3D5522C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D54E2C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift */; };
+		58A3D5532C96EFA8003F90FC /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D54F2C96EFA8003F90FC /* TempTargetRunStored+CoreDataClass.swift */; };
+		58A3D5542C96EFA8003F90FC /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D5502C96EFA8003F90FC /* TempTargetRunStored+CoreDataProperties.swift */; };
 		58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */; };
 		58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */; };
 		58D08B322C8DF88900AA37D3 /* DummyCharts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B312C8DF88900AA37D3 /* DummyCharts.swift */; };
-		58D08B342C8DF9A700AA37D3 /* CobChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B332C8DF9A700AA37D3 /* CobChart.swift */; };
-		58D08B362C8DFAC600AA37D3 /* IobChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B352C8DFAC600AA37D3 /* IobChart.swift */; };
+		58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B332C8DF9A700AA37D3 /* CobIobChart.swift */; };
 		58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B372C8DFB6000AA37D3 /* BasalChart.swift */; };
 		58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58D08B392C8DFECD00AA37D3 /* TempTargets.swift */; };
 		58F107742BD1A4D000B1A680 /* Determination+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58F107732BD1A4D000B1A680 /* Determination+helper.swift */; };
@@ -322,19 +327,29 @@
 		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */; };
 		BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD3CC0712B0B89D50013189E /* MainChartView.swift */; };
 		BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */; };
+		BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */; };
 		BD6EB2D62C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */; };
+		BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */; };
+		BD793CB22CE8033500D669AC /* TempTargetRunStored.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD793CB12CE8032E00D669AC /* TempTargetRunStored.swift */; };
 		BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */; };
 		BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */; };
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
+		BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
 		BDB899882C564509006F3298 /* ForecastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForecastChart.swift */; };
 		BDB8998A2C565D0C006F3298 /* CarbsGlucose+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
+		BDC530FF2D0F6BE300088832 /* ContactTrickManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC530FE2D0F6BE300088832 /* ContactTrickManager.swift */; };
+		BDC531122D1060FA00088832 /* ContactTrickDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC531112D1060FA00088832 /* ContactTrickDetailView.swift */; };
+		BDC531142D10611D00088832 /* AddContactTrickSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC531132D10611D00088832 /* AddContactTrickSheet.swift */; };
+		BDC531162D10629000088832 /* ContactTrickPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC531152D10629000088832 /* ContactTrickPicture.swift */; };
+		BDC531182D1062F200088832 /* ContactTrickState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC531172D1062F200088832 /* ContactTrickState.swift */; };
 		BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCAF2372C639F35002DC907 /* SettingItems.swift */; };
 		BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */; };
+		BDDAF9EF2D00554500B34E7A /* SelectionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDDAF9EE2D00553E00B34E7A /* SelectionPopoverView.swift */; };
 		BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */; };
 		BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34F822C10C5B600D51995 /* DataManager.swift */; };
 		BDF34F852C10C62E00D51995 /* GlucoseData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF34F842C10C62E00D51995 /* GlucoseData.swift */; };
@@ -367,8 +382,7 @@
 		CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3442A064973004BE681 /* BaseIntentsRequest.swift */; };
 		CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3462A064973004BE681 /* CancelTempPresetIntent.swift */; };
 		CE7CA3512A064973004BE681 /* ApplyTempPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3472A064973004BE681 /* ApplyTempPresetIntent.swift */; };
-		CE7CA3522A064973004BE681 /* ListTempPresetsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3482A064973004BE681 /* ListTempPresetsIntent.swift */; };
-		CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3492A064973004BE681 /* tempPresetIntent.swift */; };
+		CE7CA3532A064973004BE681 /* TempPresetIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA3492A064973004BE681 /* TempPresetIntent.swift */; };
 		CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA34A2A064973004BE681 /* TempPresetsIntentRequest.swift */; };
 		CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA34C2A064973004BE681 /* ListStateIntent.swift */; };
 		CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7CA34D2A064973004BE681 /* StateIntentRequest.swift */; };
@@ -449,6 +463,11 @@
 		DD32CF9E2CC824C5003686D6 /* TrioRemoteControl+Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */; };
 		DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */; };
 		DD32CFA22CC824E2003686D6 /* TrioRemoteControl+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */; };
+		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
+		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
+		DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F62CF3DA9300AB8703 /* TargetPicker.swift */; };
+		DD5DC9F92CF3DAA900AB8703 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F82CF3DAA900AB8703 /* RadioButton.swift */; };
+		DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
@@ -465,11 +484,13 @@
 		DD9ECB712CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */; };
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
+		DDB37CC52D05048F00D99BF4 /* ContactTrickStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactTrickStorage.swift */; };
+		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
-		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
-		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
-		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
-		DDD163182C4C694000CD525A /* OverrideRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163172C4C694000CD525A /* OverrideRootView.swift */; };
+		DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* AdjustmentsStateModel.swift */; };
+		DDD163142C4C68D300CD525A /* AdjustmentsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* AdjustmentsProvider.swift */; };
+		DDD163162C4C690300CD525A /* AdjustmentsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* AdjustmentsDataFlow.swift */; };
+		DDD163182C4C694000CD525A /* AdjustmentsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163172C4C694000CD525A /* AdjustmentsRootView.swift */; };
 		DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163192C4C695E00CD525A /* EditOverrideForm.swift */; };
 		DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */; };
 		DDD1631F2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DDD1631D2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodeld */; };
@@ -483,8 +504,6 @@
 		DDE179592C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */; };
 		DDE1795A2C910127003CDDB7 /* CarbEntryStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1793A2C910127003CDDB7 /* CarbEntryStored+CoreDataClass.swift */; };
 		DDE1795B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1793B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift */; };
-		DDE1795C2C910127003CDDB7 /* TempTargets+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1793C2C910127003CDDB7 /* TempTargets+CoreDataClass.swift */; };
-		DDE1795D2C910127003CDDB7 /* TempTargets+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1793D2C910127003CDDB7 /* TempTargets+CoreDataProperties.swift */; };
 		DDE1795E2C910127003CDDB7 /* PumpEventStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1793E2C910127003CDDB7 /* PumpEventStored+CoreDataClass.swift */; };
 		DDE1795F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1793F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift */; };
 		DDE179602C910127003CDDB7 /* StatsData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179402C910127003CDDB7 /* StatsData+CoreDataClass.swift */; };
@@ -497,8 +516,6 @@
 		DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */; };
 		DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */; };
 		DDE179692C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE179492C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift */; };
-		DDE1796A2C910127003CDDB7 /* TempTargetsSlider+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794A2C910127003CDDB7 /* TempTargetsSlider+CoreDataClass.swift */; };
-		DDE1796B2C910127003CDDB7 /* TempTargetsSlider+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794B2C910127003CDDB7 /* TempTargetsSlider+CoreDataProperties.swift */; };
 		DDE1796C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift */; };
 		DDE1796D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift */; };
 		DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE1794E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift */; };
@@ -525,6 +542,11 @@
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
 		E3A08AAE59538BC8A8ABE477 /* GlucoseNotificationSettingsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3260468377DA9DB4DEE9AF6D /* GlucoseNotificationSettingsDataFlow.swift */; };
+		E592A3702CEEC01E009A472C /* ContactTrickEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */; };
+		E592A3772CEEC038009A472C /* ContactTrickStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3752CEEC038009A472C /* ContactTrickStateModel.swift */; };
+		E592A3782CEEC038009A472C /* ContactTrickDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3732CEEC038009A472C /* ContactTrickDataFlow.swift */; };
+		E592A3792CEEC038009A472C /* ContactTrickRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3712CEEC038009A472C /* ContactTrickRootView.swift */; };
+		E592A37A2CEEC038009A472C /* ContactTrickProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E592A3742CEEC038009A472C /* ContactTrickProvider.swift */; };
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
 		F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; };
 		F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; };
@@ -634,7 +656,6 @@
 		110AEDE92C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsConfigStateModel.swift; sourceTree = "<group>"; };
 		118DF7642C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplyOverridePresetIntent.swift; sourceTree = "<group>"; };
 		118DF7652C5ECBC60067FEB7 /* CancelOverrideIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelOverrideIntent.swift; sourceTree = "<group>"; };
-		118DF7662C5ECBC60067FEB7 /* ListOverridePresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListOverridePresetIntent.swift; sourceTree = "<group>"; };
 		118DF7672C5ECBC60067FEB7 /* OverridePresetEntity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridePresetEntity.swift; sourceTree = "<group>"; };
 		118DF7682C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverridePresetsIntentRequest.swift; sourceTree = "<group>"; };
 		19012CDB291D2CB900FB8210 /* LoopStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStats.swift; sourceTree = "<group>"; };
@@ -924,6 +945,7 @@
 		581516A82BCEEDF800BF67D7 /* NSPredicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPredicates.swift; sourceTree = "<group>"; };
 		581AC4382BE22ED10038760C /* JSONConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONConverter.swift; sourceTree = "<group>"; };
 		58237D9D2BCF0A6B00A47A79 /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
+		5825A1BD2C97335C0046467E /* EditTempTargetForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTempTargetForm.swift; sourceTree = "<group>"; };
 		582DF9742C8CDB92001F516D /* GlucoseChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseChartView.swift; sourceTree = "<group>"; };
 		582DF9762C8CDBE7001F516D /* InsulinView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinView.swift; sourceTree = "<group>"; };
 		582DF9782C8CE1E5001F516D /* MainChartHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartHelper.swift; sourceTree = "<group>"; };
@@ -944,11 +966,16 @@
 		5864E8582C42CFAE00294306 /* DeterminationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeterminationStorage.swift; sourceTree = "<group>"; };
 		587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowView.swift; sourceTree = "<group>"; };
 		5887527B2BD986E1008B081D /* OpenAPSBattery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAPSBattery.swift; sourceTree = "<group>"; };
+		58A3D5392C96D4DE003F90FC /* AddTempTargetForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTempTargetForm.swift; sourceTree = "<group>"; };
+		58A3D5432C96DE11003F90FC /* TempTargetStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+Helper.swift"; sourceTree = "<group>"; };
+		58A3D54D2C96EFA8003F90FC /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		58A3D54E2C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
+		58A3D54F2C96EFA8003F90FC /* TempTargetRunStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataClass.swift"; sourceTree = SOURCE_ROOT; };
+		58A3D5502C96EFA8003F90FC /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = SOURCE_ROOT; };
 		58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = "<group>"; };
 		58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastView.swift; sourceTree = "<group>"; };
 		58D08B312C8DF88900AA37D3 /* DummyCharts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyCharts.swift; sourceTree = "<group>"; };
-		58D08B332C8DF9A700AA37D3 /* CobChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CobChart.swift; sourceTree = "<group>"; };
-		58D08B352C8DFAC600AA37D3 /* IobChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobChart.swift; sourceTree = "<group>"; };
+		58D08B332C8DF9A700AA37D3 /* CobIobChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CobIobChart.swift; sourceTree = "<group>"; };
 		58D08B372C8DFB6000AA37D3 /* BasalChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalChart.swift; sourceTree = "<group>"; };
 		58D08B392C8DFECD00AA37D3 /* TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargets.swift; sourceTree = "<group>"; };
 		58F107732BD1A4D000B1A680 /* Determination+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Determination+helper.swift"; sourceTree = "<group>"; };
@@ -1000,19 +1027,29 @@
 		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.swift; sourceTree = "<group>"; };
 		BD3CC0712B0B89D50013189E /* MainChartView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainChartView.swift; sourceTree = "<group>"; };
 		BD4064D02C4ED26900582F43 /* CoreDataObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataObserver.swift; sourceTree = "<group>"; };
+		BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
 		BD6EB2D52C7D049B0086BBB6 /* LiveActivityWidgetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityWidgetConfiguration.swift; sourceTree = "<group>"; };
+		BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+helper.swift"; sourceTree = "<group>"; };
+		BD793CB12CE8032E00D669AC /* TempTargetRunStored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetRunStored.swift; sourceTree = "<group>"; };
 		BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigDataFlow.swift; sourceTree = "<group>"; };
 		BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigProvider.swift; sourceTree = "<group>"; };
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
+		BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetSetup.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
 		BDB899872C564509006F3298 /* ForecastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastChart.swift; sourceTree = "<group>"; };
 		BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbsGlucose+helper.swift"; sourceTree = "<group>"; };
 		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
+		BDC530FE2D0F6BE300088832 /* ContactTrickManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickManager.swift; sourceTree = "<group>"; };
+		BDC531112D1060FA00088832 /* ContactTrickDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickDetailView.swift; sourceTree = "<group>"; };
+		BDC531132D10611D00088832 /* AddContactTrickSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactTrickSheet.swift; sourceTree = "<group>"; };
+		BDC531152D10629000088832 /* ContactTrickPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickPicture.swift; sourceTree = "<group>"; };
+		BDC531172D1062F200088832 /* ContactTrickState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickState.swift; sourceTree = "<group>"; };
 		BDCAF2372C639F35002DC907 /* SettingItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingItems.swift; sourceTree = "<group>"; };
 		BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideStored+helper.swift"; sourceTree = "<group>"; };
+		BDDAF9EE2D00553E00B34E7A /* SelectionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionPopoverView.swift; sourceTree = "<group>"; };
 		BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotification.swift; sourceTree = "<group>"; };
 		BDF34F822C10C5B600D51995 /* DataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataManager.swift; sourceTree = "<group>"; };
 		BDF34F842C10C62E00D51995 /* GlucoseData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseData.swift; sourceTree = "<group>"; };
@@ -1050,8 +1087,7 @@
 		CE7CA3442A064973004BE681 /* BaseIntentsRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseIntentsRequest.swift; sourceTree = "<group>"; };
 		CE7CA3462A064973004BE681 /* CancelTempPresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CancelTempPresetIntent.swift; sourceTree = "<group>"; };
 		CE7CA3472A064973004BE681 /* ApplyTempPresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplyTempPresetIntent.swift; sourceTree = "<group>"; };
-		CE7CA3482A064973004BE681 /* ListTempPresetsIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTempPresetsIntent.swift; sourceTree = "<group>"; };
-		CE7CA3492A064973004BE681 /* tempPresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = tempPresetIntent.swift; sourceTree = "<group>"; };
+		CE7CA3492A064973004BE681 /* TempPresetIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempPresetIntent.swift; sourceTree = "<group>"; };
 		CE7CA34A2A064973004BE681 /* TempPresetsIntentRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempPresetsIntentRequest.swift; sourceTree = "<group>"; };
 		CE7CA34C2A064973004BE681 /* ListStateIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListStateIntent.swift; sourceTree = "<group>"; };
 		CE7CA34D2A064973004BE681 /* StateIntentRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateIntentRequest.swift; sourceTree = "<group>"; };
@@ -1129,6 +1165,11 @@
 		DD32CF9D2CC824C2003686D6 /* TrioRemoteControl+Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Override.swift"; sourceTree = "<group>"; };
 		DD32CF9F2CC824D3003686D6 /* TrioRemoteControl+APNS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+APNS.swift"; sourceTree = "<group>"; };
 		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
+		DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Overrides.swift"; sourceTree = "<group>"; };
+		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
+		DD5DC9F62CF3DA9300AB8703 /* TargetPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetPicker.swift; sourceTree = "<group>"; };
+		DD5DC9F82CF3DAA900AB8703 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
+		DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Helpers.swift"; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
@@ -1145,11 +1186,15 @@
 		DD9ECB6E2CA9A0BA00AA7C45 /* RemoteControlConfigProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigProvider.swift; sourceTree = "<group>"; };
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
+		DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
+		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
+		DDB37CC42D05048F00D99BF4 /* ContactTrickStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickStorage.swift; sourceTree = "<group>"; };
+		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
-		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
-		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
-		DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
-		DDD163172C4C694000CD525A /* OverrideRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideRootView.swift; sourceTree = "<group>"; };
+		DDD163112C4C689900CD525A /* AdjustmentsStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentsStateModel.swift; sourceTree = "<group>"; };
+		DDD163132C4C68D300CD525A /* AdjustmentsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentsProvider.swift; sourceTree = "<group>"; };
+		DDD163152C4C690300CD525A /* AdjustmentsDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentsDataFlow.swift; sourceTree = "<group>"; };
+		DDD163172C4C694000CD525A /* AdjustmentsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjustmentsRootView.swift; sourceTree = "<group>"; };
 		DDD163192C4C695E00CD525A /* EditOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddOverrideForm.swift; sourceTree = "<group>"; };
 		DDD1631E2C4C6F6900CD525A /* TrioCoreDataPersistentContainer.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrioCoreDataPersistentContainer.xcdatamodel; sourceTree = "<group>"; };
@@ -1163,8 +1208,6 @@
 		DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ForecastValue+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE1793A2C910127003CDDB7 /* CarbEntryStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE1793B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CarbEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		DDE1793C2C910127003CDDB7 /* TempTargets+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargets+CoreDataClass.swift"; sourceTree = "<group>"; };
-		DDE1793D2C910127003CDDB7 /* TempTargets+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargets+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE1793E2C910127003CDDB7 /* PumpEventStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpEventStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE1793F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PumpEventStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179402C910127003CDDB7 /* StatsData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatsData+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1177,8 +1220,6 @@
 		DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenAPS_Battery+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE179492C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempBasalStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		DDE1794A2C910127003CDDB7 /* TempTargetsSlider+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetsSlider+CoreDataClass.swift"; sourceTree = "<group>"; };
-		DDE1794B2C910127003CDDB7 /* TempTargetsSlider+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetsSlider+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE1794C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		DDE1794D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OverrideRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDE1794E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrefDetermination+CoreDataClass.swift"; sourceTree = "<group>"; };
@@ -1203,6 +1244,11 @@
 		E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; };
 		E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitSample.swift; sourceTree = "<group>"; };
 		E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeStateModel.swift; sourceTree = "<group>"; };
+		E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickEntry.swift; sourceTree = "<group>"; };
+		E592A3712CEEC038009A472C /* ContactTrickRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickRootView.swift; sourceTree = "<group>"; };
+		E592A3732CEEC038009A472C /* ContactTrickDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickDataFlow.swift; sourceTree = "<group>"; };
+		E592A3742CEEC038009A472C /* ContactTrickProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickProvider.swift; sourceTree = "<group>"; };
+		E592A3752CEEC038009A472C /* ContactTrickStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickStateModel.swift; sourceTree = "<group>"; };
 		E625985B47742D498CB1681A /* GlucoseNotificationSettingsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationSettingsProvider.swift; sourceTree = "<group>"; };
 		F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = "<group>"; };
 		F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = "<group>"; };
@@ -1343,7 +1389,6 @@
 			children = (
 				118DF7642C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift */,
 				118DF7652C5ECBC60067FEB7 /* CancelOverrideIntent.swift */,
-				118DF7662C5ECBC60067FEB7 /* ListOverridePresetIntent.swift */,
 				118DF7672C5ECBC60067FEB7 /* OverridePresetEntity.swift */,
 				118DF7682C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift */,
 			);
@@ -1503,6 +1548,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				DDD163032C4C67B400CD525A /* Adjustments */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
@@ -1514,6 +1560,7 @@
 				E42231DBF0DBE2B4B92D1B15 /* CarbRatioEditor */,
 				F75CB57ED6971B46F8756083 /* CGM */,
 				0610F7D6D2EC00E3BA1569F0 /* ConfigEditor */,
+				E592A3762CEEC038009A472C /* ContactTrick */,
 				9E56E3626FAD933385101B76 /* DataTable */,
 				195D80B22AF696EE00D25097 /* DynamicSettings */,
 				DD17454C2C55CA0200211FAC /* GeneralSettings */,
@@ -1527,9 +1574,7 @@
 				5031FE61F63C2A8A8B7674DD /* ManualTempBasal */,
 				19D466A129AA2B0A004D5F33 /* MealSettings */,
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
-				DDD163032C4C67B400CD525A /* OverrideConfig */,
 				99C01B871ACAB3F32CE755C7 /* PumpConfig */,
-				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
 				3811DE3825C9D4A100A708ED /* Settings */,
 				110AEDEA2C51A0AE00615CC9 /* ShortcutsConfig */,
@@ -1652,18 +1697,20 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
-				6B1A8D2C2B156EC100E76752 /* LiveActivity */,
+				3811DE9225C9D88200A708ED /* Appearance */,
 				CEB434E128B8F9BC00B70274 /* Bluetooth */,
-				F90692A8274B7A980037068D /* HealthKit */,
-				38E8754D275556E100975559 /* WatchManager */,
-				38E87406274F9AA500975559 /* UserNotifications */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
-				38AEE75025F021F10013F05B /* SettingsManager */,
-				38B4F3C425E5016800E76A18 /* Notifications */,
-				3811DE9225C9D88200A708ED /* Appearance */,
+				E592A37E2CEEC046009A472C /* ContactTrick */,
+				F90692A8274B7A980037068D /* HealthKit */,
+				6B1A8D2C2B156EC100E76752 /* LiveActivity */,
 				3811DE9425C9D88200A708ED /* Network */,
+				38B4F3C425E5016800E76A18 /* Notifications */,
+				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
+				38AEE75025F021F10013F05B /* SettingsManager */,
 				3811DE9825C9D88300A708ED /* Storage */,
 				3811DEA525C9D88300A708ED /* UnlockManager */,
+				38E87406274F9AA500975559 /* UserNotifications */,
+				38E8754D275556E100975559 /* WatchManager */,
 			);
 			path = Services;
 			sourceTree = "<group>";
@@ -1682,9 +1729,8 @@
 				38E44521274E3DDC00EC9A94 /* NetworkReachabilityManager.swift */,
 				38192E03261B82FA0094D973 /* ReachabilityManager.swift */,
 				3811DE9625C9D88300A708ED /* HTTPResponseStatus.swift */,
-				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
+				DDC9B9962CFD2332003E7721 /* Nightscout */,
 				38FE826925CC82DB001FF17A /* NetworkService.swift */,
-				38FE826C25CC8461001FF17A /* NightscoutAPI.swift */,
 				CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */,
 			);
 			path = Network;
@@ -1748,6 +1794,7 @@
 			children = (
 				38E4451D274DB04600EC9A94 /* AppDelegate.swift */,
 				388E595B25AD948C0019842D /* FreeAPSApp.swift */,
+				BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */,
 			);
 			path = Application;
 			sourceTree = "<group>";
@@ -1835,16 +1882,7 @@
 			isa = PBXGroup;
 			children = (
 				BD3CC0712B0B89D50013189E /* MainChartView.swift */,
-				582DF9742C8CDB92001F516D /* GlucoseChartView.swift */,
-				582DF9762C8CDBE7001F516D /* InsulinView.swift */,
-				582DF97A2C8CE209001F516D /* CarbView.swift */,
-				58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */,
-				58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */,
-				58D08B312C8DF88900AA37D3 /* DummyCharts.swift */,
-				58D08B332C8DF9A700AA37D3 /* CobChart.swift */,
-				58D08B352C8DFAC600AA37D3 /* IobChart.swift */,
-				58D08B372C8DFB6000AA37D3 /* BasalChart.swift */,
-				58D08B392C8DFECD00AA37D3 /* TempTargets.swift */,
+				BDDAF9F12D0055CC00B34E7A /* ChartElements */,
 			);
 			path = Chart;
 			sourceTree = "<group>";
@@ -1964,6 +2002,7 @@
 				38A9260425F012D8009E3739 /* CarbRatios.swift */,
 				38D0B3D825EC07C400CB6E88 /* CarbsEntry.swift */,
 				3811DF0125CA9FEA00A708ED /* Credentials.swift */,
+				E592A36F2CEEC01E009A472C /* ContactTrickEntry.swift */,
 				38AEE73C25F0200C0013F05B /* FreeAPSSettings.swift */,
 				383948D925CD64D500E91849 /* Glucose.swift */,
 				382C133625F13A1E00715CE1 /* InsulinSensitivities.swift */,
@@ -2044,6 +2083,7 @@
 		38A0362725ECF05300FCBB52 /* Storage */ = {
 			isa = PBXGroup;
 			children = (
+				DDB37CC42D05048F00D99BF4 /* ContactTrickStorage.swift */,
 				385CEAC325F2F154002D6D5B /* AnnouncementsStorage.swift */,
 				38AEE75625F0F18E0013F05B /* CarbsStorage.swift */,
 				38A0363A25ECF07E00FCBB52 /* GlucoseStorage.swift */,
@@ -2059,6 +2099,7 @@
 		38A504F625DDA0E200C5B9E8 /* Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				DDB37CC62D05127500D99BF4 /* FontExtensions.swift */,
 				38A5049125DD9C4000C5B9E8 /* UserDefaultsExtensions.swift */,
 				38BF021625E7CBBC00579895 /* PumpManagerExtensions.swift */,
 				CEB434E628B9053300B70274 /* LoopUIColorPalette+Default.swift */,
@@ -2303,7 +2344,10 @@
 				582FAE422C05102C00D1C13F /* CoreDataError.swift */,
 				BDF34EBD2C0A31D000D51995 /* CustomNotification.swift */,
 				BDCD47AE2C1F3F1700F8BCD5 /* OverrideStored+helper.swift */,
+				BD793CAF2CE7C60E00D669AC /* OverrideRunStored+helper.swift */,
 				BDB899892C565D0B006F3298 /* CarbsGlucose+helper.swift */,
+				58A3D5432C96DE11003F90FC /* TempTargetStored+Helper.swift */,
+				BD793CB12CE8032E00D669AC /* TempTargetRunStored.swift */,
 			);
 			path = Helper;
 			sourceTree = "<group>";
@@ -2311,6 +2355,7 @@
 		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
 				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
 				58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */,
@@ -2433,6 +2478,24 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BD793CAD2CE7660C00D669AC /* Overrides */ = {
+			isa = PBXGroup;
+			children = (
+				DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */,
+				DDD163192C4C695E00CD525A /* EditOverrideForm.swift */,
+			);
+			path = Overrides;
+			sourceTree = "<group>";
+		};
+		BD793CAE2CE7661D00D669AC /* TempTargets */ = {
+			isa = PBXGroup;
+			children = (
+				58A3D5392C96D4DE003F90FC /* AddTempTargetForm.swift */,
+				5825A1BD2C97335C0046467E /* EditTempTargetForm.swift */,
+			);
+			path = TempTargets;
+			sourceTree = "<group>";
+		};
 		BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */ = {
 			isa = PBXGroup;
 			children = (
@@ -2452,6 +2515,23 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		BDDAF9F12D0055CC00B34E7A /* ChartElements */ = {
+			isa = PBXGroup;
+			children = (
+				BDDAF9EE2D00553E00B34E7A /* SelectionPopoverView.swift */,
+				582DF9742C8CDB92001F516D /* GlucoseChartView.swift */,
+				582DF9762C8CDBE7001F516D /* InsulinView.swift */,
+				582DF97A2C8CE209001F516D /* CarbView.swift */,
+				58D08B212C8DAA8E00AA37D3 /* OverrideView.swift */,
+				58D08B2F2C8DEA7500AA37D3 /* ForecastView.swift */,
+				58D08B312C8DF88900AA37D3 /* DummyCharts.swift */,
+				58D08B332C8DF9A700AA37D3 /* CobIobChart.swift */,
+				58D08B372C8DFB6000AA37D3 /* BasalChart.swift */,
+				58D08B392C8DFECD00AA37D3 /* TempTargets.swift */,
+			);
+			path = ChartElements;
+			sourceTree = "<group>";
+		};
 		BDF34F882C10C65E00D51995 /* Data */ = {
 			isa = PBXGroup;
 			children = (
@@ -2500,10 +2580,9 @@
 		CE7CA3452A064973004BE681 /* TempPresets */ = {
 			isa = PBXGroup;
 			children = (
-				CE7CA3462A064973004BE681 /* CancelTempPresetIntent.swift */,
 				CE7CA3472A064973004BE681 /* ApplyTempPresetIntent.swift */,
-				CE7CA3482A064973004BE681 /* ListTempPresetsIntent.swift */,
-				CE7CA3492A064973004BE681 /* tempPresetIntent.swift */,
+				CE7CA3462A064973004BE681 /* CancelTempPresetIntent.swift */,
+				CE7CA3492A064973004BE681 /* TempPresetIntent.swift */,
 				CE7CA34A2A064973004BE681 /* TempPresetsIntentRequest.swift */,
 			);
 			path = TempPresets;
@@ -2734,6 +2813,25 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		DD5DC9EF2CF3D95400AB8703 /* AdjustmentsStateModel+Extensions */ = {
+			isa = PBXGroup;
+			children = (
+				DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */,
+				DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */,
+				DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */,
+			);
+			path = "AdjustmentsStateModel+Extensions";
+			sourceTree = "<group>";
+		};
+		DD5DC9F52CF3DA8900AB8703 /* ViewElements */ = {
+			isa = PBXGroup;
+			children = (
+				DD5DC9F82CF3DAA900AB8703 /* RadioButton.swift */,
+				DD5DC9F62CF3DA9300AB8703 /* TargetPicker.swift */,
+			);
+			path = ViewElements;
+			sourceTree = "<group>";
+		};
 		DD6B7CB72C7BAC1B00B75029 /* ProfileImport */ = {
 			isa = PBXGroup;
 			children = (
@@ -2776,23 +2874,34 @@
 			path = View;
 			sourceTree = "<group>";
 		};
-		DDD163032C4C67B400CD525A /* OverrideConfig */ = {
+		DDC9B9962CFD2332003E7721 /* Nightscout */ = {
 			isa = PBXGroup;
 			children = (
+				3811DE9725C9D88300A708ED /* NightscoutManager.swift */,
+				38FE826C25CC8461001FF17A /* NightscoutAPI.swift */,
+			);
+			path = Nightscout;
+			sourceTree = "<group>";
+		};
+		DDD163032C4C67B400CD525A /* Adjustments */ = {
+			isa = PBXGroup;
+			children = (
+				DD5DC9EF2CF3D95400AB8703 /* AdjustmentsStateModel+Extensions */,
 				DDD1630A2C4C67F000CD525A /* View */,
-				DDD163112C4C689900CD525A /* OverrideStateModel.swift */,
-				DDD163132C4C68D300CD525A /* OverrideProvider.swift */,
-				DDD163152C4C690300CD525A /* OverrideDataFlow.swift */,
+				DDD163112C4C689900CD525A /* AdjustmentsStateModel.swift */,
+				DDD163132C4C68D300CD525A /* AdjustmentsProvider.swift */,
+				DDD163152C4C690300CD525A /* AdjustmentsDataFlow.swift */,
 			);
-			path = OverrideConfig;
+			path = Adjustments;
 			sourceTree = "<group>";
 		};
 		DDD1630A2C4C67F000CD525A /* View */ = {
 			isa = PBXGroup;
 			children = (
-				DDD163172C4C694000CD525A /* OverrideRootView.swift */,
-				DDD163192C4C695E00CD525A /* EditOverrideForm.swift */,
-				DDD1631B2C4C697400CD525A /* AddOverrideForm.swift */,
+				DD5DC9F52CF3DA8900AB8703 /* ViewElements */,
+				DDD163172C4C694000CD525A /* AdjustmentsRootView.swift */,
+				BD793CAD2CE7660C00D669AC /* Overrides */,
+				BD793CAE2CE7661D00D669AC /* TempTargets */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2800,38 +2909,40 @@
 		DDE179112C9100FA003CDDB7 /* Classes+Properties */ = {
 			isa = PBXGroup;
 			children = (
-				DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */,
-				DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */,
-				DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */,
-				DDE179352C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift */,
 				DDE179362C910127003CDDB7 /* BolusStored+CoreDataClass.swift */,
 				DDE179372C910127003CDDB7 /* BolusStored+CoreDataProperties.swift */,
-				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
-				DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */,
 				DDE1793A2C910127003CDDB7 /* CarbEntryStored+CoreDataClass.swift */,
 				DDE1793B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift */,
-				DDE1793C2C910127003CDDB7 /* TempTargets+CoreDataClass.swift */,
-				DDE1793D2C910127003CDDB7 /* TempTargets+CoreDataProperties.swift */,
-				DDE1793E2C910127003CDDB7 /* PumpEventStored+CoreDataClass.swift */,
-				DDE1793F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift */,
-				DDE179402C910127003CDDB7 /* StatsData+CoreDataClass.swift */,
-				DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */,
+				DDB37CC22D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataClass.swift */,
+				DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */,
 				DDE179422C910127003CDDB7 /* Forecast+CoreDataClass.swift */,
 				DDE179432C910127003CDDB7 /* Forecast+CoreDataProperties.swift */,
+				DDE179382C910127003CDDB7 /* ForecastValue+CoreDataClass.swift */,
+				DDE179392C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift */,
 				DDE179442C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift */,
 				DDE179452C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift */,
+				DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */,
+				DDE179352C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift */,
+				DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */,
+				DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */,
 				DDE179462C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift */,
 				DDE179472C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift */,
-				DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */,
-				DDE179492C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift */,
-				DDE1794A2C910127003CDDB7 /* TempTargetsSlider+CoreDataClass.swift */,
-				DDE1794B2C910127003CDDB7 /* TempTargetsSlider+CoreDataProperties.swift */,
-				DDE1794C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift */,
-				DDE1794D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift */,
 				DDE1794E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift */,
 				DDE1794F2C910127003CDDB7 /* OrefDetermination+CoreDataProperties.swift */,
+				DDE1794C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift */,
+				DDE1794D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift */,
 				DDE179502C910127003CDDB7 /* OverrideStored+CoreDataClass.swift */,
 				DDE179512C910127003CDDB7 /* OverrideStored+CoreDataProperties.swift */,
+				DDE1793E2C910127003CDDB7 /* PumpEventStored+CoreDataClass.swift */,
+				DDE1793F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift */,
+				DDE179402C910127003CDDB7 /* StatsData+CoreDataClass.swift */,
+				DDE179412C910127003CDDB7 /* StatsData+CoreDataProperties.swift */,
+				DDE179482C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift */,
+				DDE179492C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift */,
+				58A3D54F2C96EFA8003F90FC /* TempTargetRunStored+CoreDataClass.swift */,
+				58A3D5502C96EFA8003F90FC /* TempTargetRunStored+CoreDataProperties.swift */,
+				58A3D54D2C96EFA8003F90FC /* TempTargetStored+CoreDataClass.swift */,
+				58A3D54E2C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift */,
 			);
 			path = "Classes+Properties";
 			sourceTree = "<group>";
@@ -2880,6 +2991,37 @@
 			path = CarbRatioEditor;
 			sourceTree = "<group>";
 		};
+		E592A3722CEEC038009A472C /* View */ = {
+			isa = PBXGroup;
+			children = (
+				E592A3712CEEC038009A472C /* ContactTrickRootView.swift */,
+				BDC531112D1060FA00088832 /* ContactTrickDetailView.swift */,
+				BDC531132D10611D00088832 /* AddContactTrickSheet.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		E592A3762CEEC038009A472C /* ContactTrick */ = {
+			isa = PBXGroup;
+			children = (
+				E592A3722CEEC038009A472C /* View */,
+				E592A3732CEEC038009A472C /* ContactTrickDataFlow.swift */,
+				E592A3742CEEC038009A472C /* ContactTrickProvider.swift */,
+				E592A3752CEEC038009A472C /* ContactTrickStateModel.swift */,
+			);
+			path = ContactTrick;
+			sourceTree = "<group>";
+		};
+		E592A37E2CEEC046009A472C /* ContactTrick */ = {
+			isa = PBXGroup;
+			children = (
+				BDC530FE2D0F6BE300088832 /* ContactTrickManager.swift */,
+				BDC531152D10629000088832 /* ContactTrickPicture.swift */,
+				BDC531172D1062F200088832 /* ContactTrickState.swift */,
+			);
+			path = ContactTrick;
+			sourceTree = "<group>";
+		};
 		EEC747824D6593B5CD87E195 /* View */ = {
 			isa = PBXGroup;
 			children = (
@@ -3267,6 +3409,7 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */,
 				3811DE2325C9D48300A708ED /* MainDataFlow.swift in Sources */,
 				BD3CC0722B0B89D50013189E /* MainChartView.swift in Sources */,
 				3811DEEB25CA063400A708ED /* PersistedProperty.swift in Sources */,
@@ -3274,6 +3417,7 @@
 				388E5A6025B6F2310019842D /* Autosens.swift in Sources */,
 				DD9ECB6A2CA99F6C00AA7C45 /* PushMessage.swift in Sources */,
 				3811DE8F25C9D80400A708ED /* User.swift in Sources */,
+				5825A1BE2C97335C0046467E /* EditTempTargetForm.swift in Sources */,
 				19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
 				3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
 				385CEAC425F2F154002D6D5B /* AnnouncementsStorage.swift in Sources */,
@@ -3286,6 +3430,7 @@
 				CE7CA3552A064973004BE681 /* ListStateIntent.swift in Sources */,
 				BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */,
 				195D80B72AF697B800D25097 /* DynamicSettingsDataFlow.swift in Sources */,
+				58A3D5512C96EFA8003F90FC /* TempTargetStored+CoreDataClass.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				CEA4F62329BE10F70011ADF7 /* SavitzkyGolayFilter.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
@@ -3329,6 +3474,7 @@
 				DD1745552C55CA6C00211FAC /* UnitsLimitsSettingsRootView.swift in Sources */,
 				384E803825C388640086DB71 /* Script.swift in Sources */,
 				CE94597E29E9E1EE0047C9C6 /* GarminManager.swift in Sources */,
+				58A3D5542C96EFA8003F90FC /* TempTargetRunStored+CoreDataProperties.swift in Sources */,
 				3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */,
 				38DAB280260CBB7F00F74C1A /* PumpView.swift in Sources */,
 				DDD1631C2C4C697400CD525A /* AddOverrideForm.swift in Sources */,
@@ -3337,6 +3483,7 @@
 				CE95BF572BA5F5FE00DC3DE3 /* PluginManager.swift in Sources */,
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */,
+				DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */,
 				383948D625CD4D8900E91849 /* FileStorage.swift in Sources */,
 				CEE9A6572BBB418300EB5194 /* CalibrationsChart.swift in Sources */,
 				3811DE4125C9D4A100A708ED /* SettingsRootView.swift in Sources */,
@@ -3367,6 +3514,7 @@
 				389A572026079BAA00BC102F /* Interpolation.swift in Sources */,
 				DD9ECB702CA9A0BA00AA7C45 /* RemoteControlConfigStateModel.swift in Sources */,
 				19A910382A24EF3200C8951B /* ChartsView.swift in Sources */,
+				58A3D5522C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift in Sources */,
 				DD32CF9A2CC8247B003686D6 /* TrioRemoteControl+Meal.swift in Sources */,
 				BDF34F832C10C5B600D51995 /* DataManager.swift in Sources */,
 				38B4F3C625E5017E00E76A18 /* NotificationCenter.swift in Sources */,
@@ -3408,8 +3556,10 @@
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,
 				58D08B222C8DAA8E00AA37D3 /* OverrideView.swift in Sources */,
 				BD0B2EF32C5998E600B3298F /* MealPresetView.swift in Sources */,
+				E592A3702CEEC01E009A472C /* ContactTrickEntry.swift in Sources */,
 				DD6D67E42C9C253500660C9B /* ColorSchemeOption.swift in Sources */,
 				582DF9752C8CDB92001F516D /* GlucoseChartView.swift in Sources */,
+				58A3D53A2C96D4DE003F90FC /* AddTempTargetForm.swift in Sources */,
 				DD1745302C55AE5300211FAC /* TargetBehaviorProvider.swift in Sources */,
 				58D08B382C8DFB6000AA37D3 /* BasalChart.swift in Sources */,
 				118DF76D2C5ECBC60067FEB7 /* OverridePresetEntity.swift in Sources */,
@@ -3417,6 +3567,7 @@
 				DD17454E2C55CA4D00211FAC /* UnitsLimitsSettingsDataFlow.swift in Sources */,
 				DDF847E62C5D66490049BB3B /* AddMealPresetView.swift in Sources */,
 				3811DEAE25C9D88300A708ED /* Cache.swift in Sources */,
+				BDA6CC882CAF219B00F942F9 /* TempTargetSetup.swift in Sources */,
 				383420D625FFE38C002D46C1 /* LoopView.swift in Sources */,
 				DD1745192C543B5700211FAC /* NotificationsView.swift in Sources */,
 				3811DEAD25C9D88300A708ED /* UserDefaults+Cache.swift in Sources */,
@@ -3434,10 +3585,10 @@
 				CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */,
 				3811DE3525C9D49500A708ED /* HomeRootView.swift in Sources */,
 				38E98A2925F52C9300C0CED0 /* Error+Extensions.swift in Sources */,
-				118DF76C2C5ECBC60067FEB7 /* ListOverridePresetIntent.swift in Sources */,
 				38EA05DA261F6E7C0064E39B /* SimpleLogReporter.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
+				BD793CB22CE8033500D669AC /* TempTargetRunStored.swift in Sources */,
 				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
 				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
@@ -3445,11 +3596,12 @@
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
 				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
+				BD793CB02CE7C61500D669AC /* OverrideRunStored+helper.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				DD32CF9C2CC82499003686D6 /* TrioRemoteControl+TempTarget.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
-				DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */,
+				DDD163142C4C68D300CD525A /* AdjustmentsProvider.swift in Sources */,
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
 				191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
@@ -3482,7 +3634,6 @@
 				BDCD47AF2C1F3F1700F8BCD5 /* OverrideStored+helper.swift in Sources */,
 				3811DE3F25C9D4A100A708ED /* SettingsStateModel.swift in Sources */,
 				CE7CA3582A064E2F004BE681 /* ListStateView.swift in Sources */,
-				58D08B362C8DFAC600AA37D3 /* IobChart.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
@@ -3508,7 +3659,7 @@
 				E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */,
 				45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */,
 				DD32CFA02CC824D6003686D6 /* TrioRemoteControl+APNS.swift in Sources */,
-				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
+				CE7CA3532A064973004BE681 /* TempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				DD6B7CBB2C7FBBFA00B75029 /* ReviewInsulinActionView.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
@@ -3524,7 +3675,7 @@
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				DD1745442C55C60E00211FAC /* AutosensSettingsDataFlow.swift in Sources */,
 				BDCAF2382C639F35002DC907 /* SettingItems.swift in Sources */,
-				58D08B342C8DF9A700AA37D3 /* CobChart.swift in Sources */,
+				58D08B342C8DF9A700AA37D3 /* CobIobChart.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
@@ -3535,7 +3686,6 @@
 				E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */,
 				DD1745502C55CA5500211FAC /* UnitsLimitsSettingsProvider.swift in Sources */,
 				581AC4392BE22ED10038760C /* JSONConverter.swift in Sources */,
-				CE7CA3522A064973004BE681 /* ListTempPresetsIntent.swift in Sources */,
 				BD4064D12C4ED26900582F43 /* CoreDataObserver.swift in Sources */,
 				58645B9B2CA2D24F008AFCE7 /* CarbSetup.swift in Sources */,
 				38E44536274E411700EC9A94 /* Disk.swift in Sources */,
@@ -3575,10 +3725,12 @@
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DD17454B2C55C62800211FAC /* AutosensSettingsRootView.swift in Sources */,
 				DDF847DF2C5C28780049BB3B /* LiveActivitySettingsProvider.swift in Sources */,
+				DDB37CC52D05048F00D99BF4 /* ContactTrickStorage.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
+				DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */,
 				582DF9772C8CDBE7001F516D /* InsulinView.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */,
@@ -3604,7 +3756,10 @@
 				58D08B3A2C8DFECD00AA37D3 /* TempTargets.swift in Sources */,
 				38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */,
 				CE7CA3542A064973004BE681 /* TempPresetsIntentRequest.swift in Sources */,
+				58A3D5442C96DE11003F90FC /* TempTargetStored+Helper.swift in Sources */,
+				58A3D5532C96EFA8003F90FC /* TempTargetRunStored+CoreDataClass.swift in Sources */,
 				DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */,
+				DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */,
 				F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */,
 				DD17453A2C55BFA600211FAC /* AlgorithmAdvancedSettingsDataFlow.swift in Sources */,
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
@@ -3612,7 +3767,7 @@
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				38E8754F275556FA00975559 /* WatchManager.swift in Sources */,
-				DDD163182C4C694000CD525A /* OverrideRootView.swift in Sources */,
+				DDD163182C4C694000CD525A /* AdjustmentsRootView.swift in Sources */,
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
@@ -3620,12 +3775,18 @@
 				DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */,
 				CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */,
 				38E8755427561E9800975559 /* DataFlow.swift in Sources */,
+				DD5DC9F92CF3DAA900AB8703 /* RadioButton.swift in Sources */,
 				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
 				CE7CA34F2A064973004BE681 /* BaseIntentsRequest.swift in Sources */,
+				E592A3772CEEC038009A472C /* ContactTrickStateModel.swift in Sources */,
+				E592A3782CEEC038009A472C /* ContactTrickDataFlow.swift in Sources */,
+				E592A3792CEEC038009A472C /* ContactTrickRootView.swift in Sources */,
+				BDC531182D1062F200088832 /* ContactTrickState.swift in Sources */,
+				E592A37A2CEEC038009A472C /* ContactTrickProvider.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */,
-				DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */,
+				DDD163162C4C690300CD525A /* AdjustmentsDataFlow.swift in Sources */,
 				BDF34F932C10D0E100D51995 /* LiveActivityAttributes+Helper.swift in Sources */,
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
@@ -3640,6 +3801,7 @@
 				69A31254F2451C20361D172F /* TreatmentsStateModel.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
+				BDC531162D10629000088832 /* ContactTrickPicture.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				BDF34F852C10C62E00D51995 /* GlucoseData.swift in Sources */,
@@ -3657,7 +3819,7 @@
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
 				38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */,
 				38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */,
-				DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */,
+				DDD163122C4C689900CD525A /* AdjustmentsStateModel.swift in Sources */,
 				3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */,
 				DD1745132C54169400211FAC /* DevicesView.swift in Sources */,
 				891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */,
@@ -3682,6 +3844,7 @@
 				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
+				DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,
@@ -3696,6 +3859,8 @@
 				DDE179522C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift in Sources */,
 				DDE179532C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift in Sources */,
 				DDE179542C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift in Sources */,
+				BDC530FF2D0F6BE300088832 /* ContactTrickManager.swift in Sources */,
+				BDC531122D1060FA00088832 /* ContactTrickDetailView.swift in Sources */,
 				DDE179552C910127003CDDB7 /* LoopStatRecord+CoreDataProperties.swift in Sources */,
 				DDE179562C910127003CDDB7 /* BolusStored+CoreDataClass.swift in Sources */,
 				DDE179572C910127003CDDB7 /* BolusStored+CoreDataProperties.swift in Sources */,
@@ -3703,9 +3868,8 @@
 				DDE179592C910127003CDDB7 /* ForecastValue+CoreDataProperties.swift in Sources */,
 				DDE1795A2C910127003CDDB7 /* CarbEntryStored+CoreDataClass.swift in Sources */,
 				DDE1795B2C910127003CDDB7 /* CarbEntryStored+CoreDataProperties.swift in Sources */,
-				DDE1795C2C910127003CDDB7 /* TempTargets+CoreDataClass.swift in Sources */,
-				DDE1795D2C910127003CDDB7 /* TempTargets+CoreDataProperties.swift in Sources */,
 				DDE1795E2C910127003CDDB7 /* PumpEventStored+CoreDataClass.swift in Sources */,
+				BDDAF9EF2D00554500B34E7A /* SelectionPopoverView.swift in Sources */,
 				DDE1795F2C910127003CDDB7 /* PumpEventStored+CoreDataProperties.swift in Sources */,
 				DDE179602C910127003CDDB7 /* StatsData+CoreDataClass.swift in Sources */,
 				DDE179612C910127003CDDB7 /* StatsData+CoreDataProperties.swift in Sources */,
@@ -3713,12 +3877,11 @@
 				DDE179632C910127003CDDB7 /* Forecast+CoreDataProperties.swift in Sources */,
 				DDE179642C910127003CDDB7 /* GlucoseStored+CoreDataClass.swift in Sources */,
 				DDE179652C910127003CDDB7 /* GlucoseStored+CoreDataProperties.swift in Sources */,
+				BDC531142D10611D00088832 /* AddContactTrickSheet.swift in Sources */,
 				DDE179662C910127003CDDB7 /* OpenAPS_Battery+CoreDataClass.swift in Sources */,
 				DDE179672C910127003CDDB7 /* OpenAPS_Battery+CoreDataProperties.swift in Sources */,
 				DDE179682C910127003CDDB7 /* TempBasalStored+CoreDataClass.swift in Sources */,
 				DDE179692C910127003CDDB7 /* TempBasalStored+CoreDataProperties.swift in Sources */,
-				DDE1796A2C910127003CDDB7 /* TempTargetsSlider+CoreDataClass.swift in Sources */,
-				DDE1796B2C910127003CDDB7 /* TempTargetsSlider+CoreDataProperties.swift in Sources */,
 				DDE1796C2C910127003CDDB7 /* OverrideRunStored+CoreDataClass.swift in Sources */,
 				DDE1796D2C910127003CDDB7 /* OverrideRunStored+CoreDataProperties.swift in Sources */,
 				DDE1796E2C910127003CDDB7 /* OrefDetermination+CoreDataClass.swift in Sources */,
@@ -3730,6 +3893,7 @@
 				CE7CA3502A064973004BE681 /* CancelTempPresetIntent.swift in Sources */,
 				6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */,
 				6FFAE524D1D9C262F2407CAE /* SnoozeProvider.swift in Sources */,
+				BD4ED4FD2CF9D5E8000EDC9C /* AppState.swift in Sources */,
 				DDF847DD2C5C28720049BB3B /* LiveActivitySettingsDataFlow.swift in Sources */,
 				8194B80890CDD6A3C13B0FEE /* SnoozeStateModel.swift in Sources */,
 				0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */,

+ 3 - 0
FreeAPS/Resources/Base.lproj/InfoPlist.strings

@@ -18,3 +18,6 @@
 
 /* Privacy - Health Share Usage Description */
 "NSHealthShareUsageDescription" = "Health App is used to store blood glucose, insulin and carbohydrates";
+
+/* Privacy - Contacts Usage Description */
+"NSContactsUsageDescription" = "Allows Trio to access your contacts for live updates to your Apple Watch contact complication using the 'Contact Trick' feature.";

+ 3 - 0
FreeAPS/Resources/Info.plist

@@ -78,6 +78,8 @@
 	<string>Calendar is used to create a new glucose events.</string>
 	<key>NSFaceIDUsageDescription</key>
 	<string>For authorized acces to bolus</string>
+	<key>NSContactsUsageDescription</key>
+	<string>Contact is used to create a Apple Watch complication</string>
 	<key>NSHealthShareUsageDescription</key>
 	<string>Health App is used to store blood glucose, carbs and insulin</string>
 	<key>NSHealthUpdateUsageDescription</key>
@@ -101,6 +103,7 @@
 		<string>bluetooth-peripheral</string>
 		<string>fetch</string>
 		<string>processing</string>
+                <string>remote-notification</string>
 	</array>
 	<key>UIFileSharingEnabled</key>
 	<true/>

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
FreeAPS/Resources/javascript/bundle/meal.js


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

@@ -207,7 +207,7 @@ final class BaseAPSManager: APSManager, Injectable {
             backGroundTaskID = await UIApplication.shared.beginBackgroundTask(withName: "Loop starting") {
                 guard let backgroundTask = self.backGroundTaskID else { return }
                 Task {
-                    await UIApplication.shared.endBackgroundTask(backgroundTask)
+                    UIApplication.shared.endBackgroundTask(backgroundTask)
                 }
                 self.backGroundTaskID = .invalid
             }
@@ -391,7 +391,7 @@ final class BaseAPSManager: APSManager, Injectable {
             let now = Date()
 
             // Start fetching asynchronously
-            let (currentTemp, profiles, autosense, dailyAutotune) = try await (
+            let (currentTemp, _, _, _) = try await (
                 fetchCurrentTempBasal(date: now),
                 makeProfiles(),
                 autosense(),

+ 87 - 0
FreeAPS/Sources/APS/Extensions/FontExtensions.swift

@@ -0,0 +1,87 @@
+import SwiftUI
+
+extension Font.Weight {
+    var displayName: String {
+        switch self {
+        case .ultraLight: return "Ultra Light"
+        case .light: return "Light"
+        case .regular: return "Regular"
+        case .medium: return "Medium"
+        case .semibold: return "Semibold"
+        case .bold: return "Bold"
+        case .heavy: return "Heavy"
+        case .black: return "Black"
+        default: return "Unknown"
+        }
+    }
+
+    private static let stringToFontWeight: [String: Font.Weight] = [
+        "ultraLight": .ultraLight,
+        "thin": .thin,
+        "light": .light,
+        "regular": .regular,
+        "medium": .medium,
+        "semibold": .semibold,
+        "bold": .bold,
+        "heavy": .heavy,
+        "black": .black
+    ]
+
+    private static let fontWeightToString: [Font.Weight: String] = [
+        .ultraLight: "ultraLight",
+        .thin: "thin",
+        .light: "light",
+        .regular: "regular",
+        .medium: "medium",
+        .semibold: "semibold",
+        .bold: "bold",
+        .heavy: "heavy",
+        .black: "black"
+    ]
+
+    /// Initialize `Font.Weight` from a string
+    static func fromString(_ string: String) -> Font.Weight {
+        stringToFontWeight[string] ?? .regular // Default fallback
+    }
+
+    /// Convert `Font.Weight` to a string
+    var asString: String {
+        Font.Weight.fontWeightToString[self] ?? "regular" // Default fallback
+    }
+}
+
+extension Font.Width {
+    var displayName: String {
+        switch self {
+        case .condensed: return "Condensed"
+        case .expanded: return "Expanded"
+        case .compressed: return "Compressed"
+        case .standard: return "Standard"
+        default: return "Unknown"
+        }
+    }
+
+    private static let stringToFontWidth: [String: Font.Width] = [
+        "compressed": .compressed,
+        "condensed": .condensed,
+        "standard": .standard,
+        "expanded": .expanded
+    ]
+
+    private static let fontWidthToString: [Font.Width: String] = [
+        .compressed: "compressed",
+        .condensed: "condensed",
+        .standard: "standard",
+        .expanded: "expanded"
+    ]
+
+    /// Initialize `Font.Width` from a string
+    static func fromString(_ string: String) -> Font.Width {
+        stringToFontWidth[string] ?? .standard // Default fallback
+    }
+
+    /// Convert `Font.Width` to a string
+    var asString: String {
+        Font.Width.fontWidthToString[self] ?? "standard" // Default fallback
+    }
+}

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

@@ -198,7 +198,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
             key: "date",
             ascending: false,
             fetchLimit: 6
-        )
+        ) as? [GlucoseStored]
     }
 
     private func processGlucose() -> [BloodGlucose] {

+ 36 - 3
FreeAPS/Sources/APS/FetchTreatmentsManager.swift

@@ -1,4 +1,5 @@
 import Combine
+import CoreData
 import Foundation
 import SwiftDate
 import Swinject
@@ -13,6 +14,7 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
+    private var backgroundContext = CoreDataStack.shared.newTaskContext()
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -28,17 +30,48 @@ final class BaseFetchTreatmentsManager: FetchTreatmentsManager, Injectable {
                 debug(.nightscout, "Start fetching carbs and temptargets")
 
                 Task {
+                    // Fetch carbs and temp targets concurrently
                     async let carbs = self.nightscoutManager.fetchCarbs()
                     async let tempTargets = self.nightscoutManager.fetchTempTargets()
 
-                    let filteredCarbs = await carbs.filter { !($0.enteredBy?.contains(CarbsEntry.manual) ?? false) }
+                    // Filter and store if not from "Trio"
+                    let filteredCarbs = await carbs.filter { $0.enteredBy != CarbsEntry.local }
                     if filteredCarbs.isNotEmpty {
                         await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
                     }
 
-                    let filteredTargets = await tempTargets.filter { !($0.enteredBy?.contains(TempTarget.manual) ?? false) }
+                    // Filter and store if not from Trio
+                    let filteredTargets = await tempTargets.filter { $0.enteredBy != TempTarget.local }
                     if filteredTargets.isNotEmpty {
-                        self.tempTargetsStorage.storeTempTargets(filteredTargets)
+                        // Sort temp targets by creation date
+                        let sortedTargets = filteredTargets.sorted { $0.createdAt < $1.createdAt }
+
+                        // Iterate and store each temp target
+                        for (index, tempTarget) in sortedTargets.enumerated() {
+                            // Skip saving if a Temp Target with the same date already exists or it's a cancel target
+                            guard await !self.tempTargetsStorage.existsTempTarget(with: tempTarget.createdAt),
+                                  tempTarget.reason != TempTarget.cancel
+                            else {
+                                debug(
+                                    .nightscout,
+                                    "Skipping temp target with date: \(tempTarget.date ?? Date.distantPast)"
+                                )
+                                continue
+                            }
+
+                            // Create a mutable copy and set enabled for the last temp target
+                            var mutableTempTarget = tempTarget
+                            mutableTempTarget.enabled = (index == sortedTargets.count - 1)
+
+                            // Save to Core Data
+                            await self.tempTargetsStorage.storeTempTarget(tempTarget: mutableTempTarget)
+                        }
+
+                        // Save the temp targets to JSON so that they get used by oref
+                        self.tempTargetsStorage.saveTempTargetsToStorage(sortedTargets)
+
+                        // Update Adjustments View
+                        Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
                     }
                 }
             }

+ 1 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -39,6 +39,7 @@ extension OpenAPS {
         static let carbRatios = "settings/carb_ratios.json"
         static let tempTargets = "settings/temptargets.json"
         static let model = "settings/model.json"
+        static let contactTrick = "settings/contact_trick.json"
     }
 
     enum Monitor {

+ 112 - 192
FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift

@@ -357,194 +357,66 @@ final class OpenAPS {
 
     func oref2() async -> RawJSON {
         await context.perform {
-            let preferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
-            var hbt_ = preferences?.halfBasalExerciseTarget ?? 160
-            let wp = preferences?.weightPercentage ?? 1
-            let smbMinutes = (preferences?.maxSMBBasalMinutes ?? 30) as NSDecimalNumber
-            let uamMinutes = (preferences?.maxUAMSMBBasalMinutes ?? 30) as NSDecimalNumber
+            // Retrieve user preferences
+            let userPreferences = self.storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
+            let weightPercentage = userPreferences?.weightPercentage ?? 1.0
+            let maxSMBBasalMinutes = userPreferences?.maxSMBBasalMinutes ?? 30
+            let maxUAMBasalMinutes = userPreferences?.maxUAMSMBBasalMinutes ?? 30
 
+            // Fetch historical events for Total Daily Dose (TDD) calculation
             let tenDaysAgo = Date().addingTimeInterval(-10.days.timeInterval)
             let twoHoursAgo = Date().addingTimeInterval(-2.hours.timeInterval)
-
-            var uniqueEvents = [[String: Any]]()
-            let requestTDD = OrefDetermination.fetchRequest() as NSFetchRequest<NSFetchRequestResult>
-            requestTDD.predicate = NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", tenDaysAgo as NSDate)
-            requestTDD.propertiesToFetch = ["timestamp", "totalDailyDose"]
-            let sortTDD = NSSortDescriptor(key: "timestamp", ascending: true)
-            requestTDD.sortDescriptors = [sortTDD]
-            requestTDD.resultType = .dictionaryResultType
-
-            do {
-                if let fetchedResults = try self.context.fetch(requestTDD) as? [[String: Any]] {
-                    uniqueEvents = fetchedResults
-                }
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch TDD Data")
-            }
-
-            var sliderArray = [TempTargetsSlider]()
-            let requestIsEnbled = TempTargetsSlider.fetchRequest() as NSFetchRequest<TempTargetsSlider>
-            let sortIsEnabled = NSSortDescriptor(key: "date", ascending: false)
-            requestIsEnbled.sortDescriptors = [sortIsEnabled]
-            // requestIsEnbled.fetchLimit = 1
-            try? sliderArray = self.context.fetch(requestIsEnbled)
-
-            /// Get the last active Override as only this information is apparently used in oref2
-            var overrideArray = [OverrideStored]()
-            let requestOverrides = OverrideStored.fetchRequest() as NSFetchRequest<OverrideStored>
-            let sortOverride = NSSortDescriptor(key: "date", ascending: false)
-            requestOverrides.sortDescriptors = [sortOverride]
-            requestOverrides.predicate = NSPredicate.lastActiveOverride
-            requestOverrides.fetchLimit = 1
-            try? overrideArray = self.context.fetch(requestOverrides)
-
-            var tempTargetsArray = [TempTargets]()
-            let requestTempTargets = TempTargets.fetchRequest() as NSFetchRequest<TempTargets>
-            let sortTT = NSSortDescriptor(key: "date", ascending: false)
-            requestTempTargets.sortDescriptors = [sortTT]
-            requestTempTargets.fetchLimit = 1
-            try? tempTargetsArray = self.context.fetch(requestTempTargets)
-
-            let total = uniqueEvents.compactMap({ ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0 }).reduce(0, +)
-            var indeces = uniqueEvents.count
-            // Only fetch once. Use same (previous) fetch
-            let twoHoursArray = uniqueEvents.filter({ ($0["timestamp"] as? Date ?? Date()) >= twoHoursAgo })
-            var nrOfIndeces = twoHoursArray.count
-            let totalAmount = twoHoursArray.compactMap({ ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0 })
+            let historicalTDDData = self.fetchHistoricalTDDData(from: tenDaysAgo)
+
+            // Fetch the last active Override
+            let activeOverrides = self.fetchActiveOverrides()
+            let isOverrideActive = activeOverrides.first?.enabled ?? false
+            let overridePercentage = Decimal(activeOverrides.first?.percentage ?? 100)
+            let isOverrideIndefinite = activeOverrides.first?.indefinite ?? true
+            let disableSMBs = activeOverrides.first?.smbIsOff ?? false
+            let overrideTargetBG = activeOverrides.first?.target?.decimalValue ?? 0
+
+            // Calculate averages for Total Daily Dose (TDD)
+            let totalTDD = historicalTDDData.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }.reduce(0, +)
+            let totalDaysCount = max(historicalTDDData.count, 1)
+
+            // Fetch recent TDD data for the past two hours
+            let recentTDDData = historicalTDDData.filter { ($0["timestamp"] as? Date ?? Date()) >= twoHoursAgo }
+            let recentDataCount = max(recentTDDData.count, 1)
+            let recentTotalTDD = recentTDDData.compactMap { ($0["totalDailyDose"] as? NSDecimalNumber)?.decimalValue }
                 .reduce(0, +)
 
-            var temptargetActive = tempTargetsArray.first?.active ?? false
-            let isPercentageEnabled = sliderArray.first?.enabled ?? false
-
-            var useOverride = overrideArray.first?.enabled ?? false
-            var overridePercentage = Decimal(overrideArray.first?.percentage ?? 100)
-            var unlimited = overrideArray.first?.indefinite ?? true
-            var disableSMBs = overrideArray.first?.smbIsOff ?? false
-
-            let currentTDD = (uniqueEvents.last?["totalDailyDose"] as? NSDecimalNumber)?.decimalValue ?? 0
-
-            if indeces == 0 {
-                indeces = 1
-            }
-            if nrOfIndeces == 0 {
-                nrOfIndeces = 1
-            }
-
-            let average2hours = totalAmount / Decimal(nrOfIndeces)
-            let average14 = total / Decimal(indeces)
-
-            let weight = wp
-            let weighted_average = weight * average2hours + (1 - weight) * average14
-
-            var duration: Decimal = 0
-            var overrideTarget: Decimal = 0
-
-            if useOverride {
-                duration = (overrideArray.first?.duration ?? 0) as Decimal
-                overrideTarget = (overrideArray.first?.target ?? 0) as Decimal
-                let advancedSettings = overrideArray.first?.advancedSettings ?? false
-                let addedMinutes = Int(duration)
-                let date = overrideArray.first?.date ?? Date()
-                if date.addingTimeInterval(addedMinutes.minutes.timeInterval) < Date(),
-                   !unlimited
-                {
-                    useOverride = false
-                    let saveToCoreData = OverrideStored(context: self.context)
-                    saveToCoreData.enabled = false
-                    saveToCoreData.date = Date()
-                    saveToCoreData.duration = 0
-                    saveToCoreData.indefinite = false
-                    saveToCoreData.percentage = 100
-                    do {
-                        guard self.context.hasChanges else { return "{}" }
-                        try self.context.save()
-                    } catch {
-                        print(error.localizedDescription)
-                    }
-                }
-            }
-
-            if !useOverride {
-                unlimited = true
-                overridePercentage = 100
-                duration = 0
-                overrideTarget = 0
-                disableSMBs = false
-            }
-
-            if temptargetActive {
-                var duration_ = 0
-                var hbt = Double(hbt_)
-                var dd = 0.0
-
-                if temptargetActive {
-                    duration_ = Int(truncating: tempTargetsArray.first?.duration ?? 0)
-                    hbt = tempTargetsArray.first?.hbt ?? Double(hbt_)
-                    let startDate = tempTargetsArray.first?.startDate ?? Date()
-                    let durationPlusStart = startDate.addingTimeInterval(duration_.minutes.timeInterval)
-                    dd = durationPlusStart.timeIntervalSinceNow.minutes
-
-                    if dd > 0.1 {
-                        hbt_ = Decimal(hbt)
-                        temptargetActive = true
-                    } else {
-                        temptargetActive = false
-                    }
-                }
-            }
-
-            if currentTDD > 0 {
-                let averages = Oref2_variables(
-                    average_total_data: average14,
-                    weightedAverage: weighted_average,
-                    past2hoursAverage: average2hours,
-                    date: Date(),
-                    isEnabled: temptargetActive,
-                    presetActive: isPercentageEnabled,
-                    overridePercentage: overridePercentage,
-                    useOverride: useOverride,
-                    duration: duration,
-                    unlimited: unlimited,
-                    hbt: hbt_,
-                    overrideTarget: overrideTarget,
-                    smbIsOff: disableSMBs,
-                    advancedSettings: overrideArray.first?.advancedSettings ?? false,
-                    isfAndCr: overrideArray.first?.isfAndCr ?? false,
-                    isf: overrideArray.first?.isf ?? false,
-                    cr: overrideArray.first?.cr ?? false,
-                    smbMinutes: (overrideArray.first?.smbMinutes ?? smbMinutes) as Decimal,
-                    uamMinutes: (overrideArray.first?.uamMinutes ?? uamMinutes) as Decimal
-                )
-
-                self.storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
-
-                return self.loadFileFromStorage(name: Monitor.oref2_variables)
+            let currentTDD = historicalTDDData.last?["totalDailyDose"] as? Decimal ?? 0
+            let averageTDDLastTwoHours = recentTotalTDD / Decimal(recentDataCount)
+            let averageTDDLastTenDays = totalTDD / Decimal(totalDaysCount)
+            let weightedTDD = weightPercentage * averageTDDLastTwoHours + (1 - weightPercentage) * averageTDDLastTenDays
+
+            // Prepare Oref2 variables
+            let oref2Data = Oref2_variables(
+                average_total_data: currentTDD > 0 ? averageTDDLastTenDays : 0,
+                weightedAverage: currentTDD > 0 ? weightedTDD : 1,
+                past2hoursAverage: currentTDD > 0 ? averageTDDLastTwoHours : 0,
+                date: Date(),
+                overridePercentage: overridePercentage,
+                useOverride: isOverrideActive,
+                duration: activeOverrides.first?.duration?.decimalValue ?? 0,
+                unlimited: isOverrideIndefinite,
+                overrideTarget: overrideTargetBG,
+                smbIsOff: disableSMBs,
+                advancedSettings: activeOverrides.first?.advancedSettings ?? false,
+                isfAndCr: activeOverrides.first?.isfAndCr ?? false,
+                isf: activeOverrides.first?.isf ?? false,
+                cr: activeOverrides.first?.cr ?? false,
+                smbIsScheduledOff: activeOverrides.first?.smbIsScheduledOff ?? false,
+                start: (activeOverrides.first?.start ?? 0) as Decimal,
+                end: (activeOverrides.first?.end ?? 0) as Decimal,
+                smbMinutes: activeOverrides.first?.smbMinutes?.decimalValue ?? maxSMBBasalMinutes,
+                uamMinutes: activeOverrides.first?.uamMinutes?.decimalValue ?? maxUAMBasalMinutes
+            )
 
-            } else {
-                let averages = Oref2_variables(
-                    average_total_data: 0,
-                    weightedAverage: 1,
-                    past2hoursAverage: 0,
-                    date: Date(),
-                    isEnabled: temptargetActive,
-                    presetActive: isPercentageEnabled,
-                    overridePercentage: overridePercentage,
-                    useOverride: useOverride,
-                    duration: duration,
-                    unlimited: unlimited,
-                    hbt: hbt_,
-                    overrideTarget: overrideTarget,
-                    smbIsOff: disableSMBs,
-                    advancedSettings: overrideArray.first?.advancedSettings ?? false,
-                    isfAndCr: overrideArray.first?.isfAndCr ?? false,
-                    isf: overrideArray.first?.isf ?? false,
-                    cr: overrideArray.first?.cr ?? false,
-                    smbMinutes: (overrideArray.first?.smbMinutes ?? smbMinutes) as Decimal,
-                    uamMinutes: (overrideArray.first?.uamMinutes ?? uamMinutes) as Decimal
-                )
-                self.storage.save(averages, as: OpenAPS.Monitor.oref2_variables)
-                return self.loadFileFromStorage(name: Monitor.oref2_variables)
-            }
+            // Save and return the Oref2 variables
+            self.storage.save(oref2Data, as: OpenAPS.Monitor.oref2_variables)
+            return self.loadFileFromStorage(name: Monitor.oref2_variables)
         }
     }
 
@@ -651,7 +523,7 @@ final class OpenAPS {
     func makeProfiles(useAutotune _: Bool) async -> Autotune? {
         debug(.openAPS, "Start makeProfiles")
 
-        async let getPreferences = loadFileFromStorageAsync(name: Settings.preferences)
+        // Load required settings and profiles asynchronously
         async let getPumpSettings = loadFileFromStorageAsync(name: Settings.settings)
         async let getBGTargets = loadFileFromStorageAsync(name: Settings.bgTargets)
         async let getBasalProfile = loadFileFromStorageAsync(name: Settings.basalProfile)
@@ -662,8 +534,7 @@ final class OpenAPS {
         async let getAutotune = loadFileFromStorageAsync(name: Settings.autotune)
         async let getFreeAPS = loadFileFromStorageAsync(name: FreeAPS.settings)
 
-        let (preferences, pumpSettings, bgTargets, basalProfile, isf, cr, tempTargets, model, autotune, freeaps) = await (
-            getPreferences,
+        let (pumpSettings, bgTargets, basalProfile, isf, cr, tempTargets, model, autotune, freeaps) = await (
             getPumpSettings,
             getBGTargets,
             getBasalProfile,
@@ -675,13 +546,26 @@ final class OpenAPS {
             getFreeAPS
         )
 
+        // Retrieve user preferences, or set defaults if not available
+        let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
+        let defaultHalfBasalTarget = preferences.halfBasalExerciseTarget
         var adjustedPreferences = preferences
-        if adjustedPreferences.isEmpty {
-            adjustedPreferences = Preferences().rawJSON
+
+        // Check for active Temp Targets and adjust HBT if necessary
+        await context.perform {
+            // Check if a Temp Target is active and if its HBT differs from user preferences
+            if let activeTempTarget = self.fetchActiveTempTargets().first,
+               activeTempTarget.enabled,
+               let activeHBT = activeTempTarget.halfBasalTarget?.decimalValue,
+               activeHBT != defaultHalfBasalTarget
+            {
+                // Overwrite the HBT in preferences
+                adjustedPreferences.halfBasalExerciseTarget = activeHBT
+                debug(.openAPS, "Updated halfBasalExerciseTarget to active Temp Target value: \(activeHBT)")
+            }
         }
 
         do {
-            // Pump Profile
             let pumpProfile = try await makeProfile(
                 preferences: adjustedPreferences,
                 pumpSettings: pumpSettings,
@@ -695,7 +579,6 @@ final class OpenAPS {
                 freeaps: freeaps
             )
 
-            // Profile
             let profile = try await makeProfile(
                 preferences: adjustedPreferences,
                 pumpSettings: pumpSettings,
@@ -709,25 +592,26 @@ final class OpenAPS {
                 freeaps: freeaps
             )
 
+            // Save the profiles
             await storage.saveAsync(pumpProfile, as: Settings.pumpProfile)
             await storage.saveAsync(profile, as: Settings.profile)
 
+            // Return the Autotune object, if available
             if let tunedProfile = Autotune(from: profile) {
                 return tunedProfile
             } else {
                 return nil
             }
         } catch {
+            // Handle errors and log failure
             debug(
                 .apsManager,
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to execute makeProfiles() to return Autoune results"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to execute makeProfiles()"
             )
             return nil
         }
     }
 
-    // MARK: - Private
-
     private func iob(pumphistory: JSON, profile: JSON, clock: JSON, autosens: JSON) async throws -> RawJSON {
         await withCheckedContinuation { continuation in
             jsWorker.inCommonContext { worker in
@@ -1020,3 +904,39 @@ final class OpenAPS {
         }
     }
 }
+
+// Non-Async fetch methods for oref2
+extension OpenAPS {
+    func fetchActiveTempTargets() -> [TempTargetStored] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: TempTargetStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastActiveTempTarget,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        ) as? [TempTargetStored] ?? []
+    }
+
+    func fetchActiveOverrides() -> [OverrideStored] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: OverrideStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastActiveOverride,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        ) as? [OverrideStored] ?? []
+    }
+
+    func fetchHistoricalTDDData(from date: Date) -> [[String: Any]] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: OrefDetermination.self,
+            onContext: context,
+            predicate: NSPredicate(format: "timestamp > %@ AND totalDailyDose > 0", date as NSDate),
+            key: "timestamp",
+            ascending: true,
+            propertiesToFetch: ["timestamp", "totalDailyDose"]
+        ) as? [[String: Any]] ?? []
+    }
+}

+ 6 - 5
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -165,7 +165,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 fat: 0,
                 protein: 0,
                 note: nil,
-                enteredBy: CarbsEntry.manual,
+                enteredBy: CarbsEntry.local,
                 isFPU: true,
                 fpuID: fpuID
             )
@@ -365,7 +365,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     rate: nil,
                     eventType: .nsCarbCorrection,
                     createdAt: result.date,
-                    enteredBy: CarbsEntry.manual,
+                    enteredBy: CarbsEntry.local,
                     bolus: nil,
                     insulin: nil,
                     notes: result.note,
@@ -402,9 +402,10 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     rate: nil,
                     eventType: .nsCarbCorrection,
                     createdAt: result.date,
-                    enteredBy: CarbsEntry.manual,
+                    enteredBy: CarbsEntry.local,
                     bolus: nil,
                     insulin: nil,
+                    notes: result.note,
                     carbs: Decimal(result.carbs),
                     fat: Decimal(result.fat),
                     protein: Decimal(result.protein),
@@ -440,7 +441,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     fat: Decimal(result.fat),
                     protein: Decimal(result.protein),
                     note: result.note,
-                    enteredBy: CarbsEntry.manual,
+                    enteredBy: CarbsEntry.local,
                     isFPU: result.isFPU,
                     fpuID: result.fpuID?.uuidString
                 )
@@ -471,7 +472,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     fat: nil,
                     protein: nil,
                     note: result.note,
-                    enteredBy: CarbsEntry.manual,
+                    enteredBy: CarbsEntry.local,
                     isFPU: nil,
                     fpuID: nil
                 )

+ 150 - 0
FreeAPS/Sources/APS/Storage/ContactTrickStorage.swift

@@ -0,0 +1,150 @@
+import CoreData
+import Foundation
+import SwiftUI
+import Swinject
+
+protocol ContactTrickStorage {
+    func fetchContactTrickEntries() async -> [ContactTrickEntry]
+    func storeContactTrickEntry(_ entry: ContactTrickEntry) async
+    func updateContactTrickEntry(_ contactTrickEntry: ContactTrickEntry) async
+    func deleteContactTrickEntry(_ objectID: NSManagedObjectID) async
+}
+
+final class BaseContactTrickStorage: ContactTrickStorage, Injectable {
+    @Injected() private var settingsManager: SettingsManager!
+
+    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+    }
+
+    /// Fetches all stored Contact Trick entries.
+    ///
+    /// The method retrieves `ContactTrickEntryStored` objects from Core Data, maps them to
+    /// `ContactTrickEntry` objects, and returns the results.
+    ///
+    /// - Returns: An array of `ContactTrickEntry` objects.
+    func fetchContactTrickEntries() async -> [ContactTrickEntry] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: ContactTrickEntryStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.all,
+            key: "hasHighContrast",
+            ascending: false
+        )
+
+        return await backgroundContext.perform {
+            guard let fetchedContactTrickEntries = results as? [ContactTrickEntryStored] else { return [] }
+
+            return fetchedContactTrickEntries.compactMap { entry in
+                ContactTrickEntry(
+                    name: entry.name ?? "No name provided",
+                    layout: ContactTrickLayout(rawValue: entry.layout ?? "Single") ?? .single,
+                    ring: ContactTrickLargeRing(rawValue: entry.ring ?? "Hidden") ?? .none,
+                    primary: ContactTrickValue(rawValue: entry.primary ?? "Glucose Reading") ?? .glucose,
+                    top: ContactTrickValue(rawValue: entry.top ?? "None") ?? .none,
+                    bottom: ContactTrickValue(rawValue: entry.bottom ?? "None") ?? .none,
+                    contactId: entry.contactId?.string,
+                    hasHighContrast: entry.hasHighContrast,
+                    ringWidth: ContactTrickEntry.RingWidth(rawValue: Int(entry.ringWidth)) ?? .regular,
+                    ringGap: ContactTrickEntry.RingGap(rawValue: Int(entry.ringGap)) ?? .small,
+                    fontSize: ContactTrickEntry.FontSize(rawValue: Int(entry.fontSize)) ?? .regular,
+                    secondaryFontSize: ContactTrickEntry.FontSize(rawValue: Int(entry.fontSizeSecondary)) ?? .small,
+                    fontWeight: Font.Weight.fromString(entry.fontWeight ?? "regular"),
+                    fontWidth: Font.Width.fromString(entry.fontWidth ?? "standard"),
+                    managedObjectID: entry.objectID
+                )
+            }
+        }
+    }
+
+    /// Stores a new Contact Trick entry.
+    ///
+    /// This method creates a new `ContactTrickEntryStored` object in the background context,
+    /// populates its properties with the values from the provided `ContactTrickEntry`, and
+    /// saves the context if changes exist.
+    ///
+    /// - Parameter contactTrickEntry: The `ContactTrickEntry` object to be stored.
+    func storeContactTrickEntry(_ contactTrickEntry: ContactTrickEntry) async {
+        await backgroundContext.perform {
+            let newContactTrickEntry = ContactTrickEntryStored(context: self.backgroundContext)
+
+            newContactTrickEntry.id = UUID()
+            newContactTrickEntry.name = contactTrickEntry.name
+            newContactTrickEntry.contactId = contactTrickEntry.contactId
+            newContactTrickEntry.layout = contactTrickEntry.layout.rawValue
+            newContactTrickEntry.ring = contactTrickEntry.ring.rawValue
+            newContactTrickEntry.primary = contactTrickEntry.primary.rawValue
+            newContactTrickEntry.top = contactTrickEntry.top.rawValue
+            newContactTrickEntry.bottom = contactTrickEntry.bottom.rawValue
+            newContactTrickEntry.hasHighContrast = contactTrickEntry.hasHighContrast
+            newContactTrickEntry.ringWidth = Int16(contactTrickEntry.ringWidth.rawValue)
+            newContactTrickEntry.ringGap = Int16(contactTrickEntry.ringGap.rawValue)
+            newContactTrickEntry.fontSize = Int16(contactTrickEntry.fontSize.rawValue)
+            newContactTrickEntry.fontSizeSecondary = Int16(contactTrickEntry.secondaryFontSize.rawValue)
+            newContactTrickEntry.fontWidth = contactTrickEntry.fontWeight.asString
+            newContactTrickEntry.fontWeight = contactTrickEntry.fontWidth.asString
+
+            do {
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Contact Trick Entry to Core Data with error: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    /// Updates an existing Contact Trick entry in Core Data.
+    ///
+    /// This method finds the existing `ContactTrickEntryStored` object by its `contactId` and updates
+    /// its properties with the values from the provided `ContactTrickEntry`. If no matching entry exists,
+    /// it does nothing.
+    ///
+    /// - Parameter contactTrickEntry: The `ContactTrickEntry` object with updated values.
+    func updateContactTrickEntry(_ contactTrickEntry: ContactTrickEntry) async {
+        await backgroundContext.perform {
+            let fetchRequest: NSFetchRequest<ContactTrickEntryStored> = ContactTrickEntryStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "contactId == %@", contactTrickEntry.contactId ?? "")
+
+            do {
+                if let existingEntry = try self.backgroundContext.fetch(fetchRequest).first {
+                    // Update the properties of the existing entry
+                    existingEntry.name = contactTrickEntry.name
+                    existingEntry.layout = contactTrickEntry.layout.rawValue
+                    existingEntry.ring = contactTrickEntry.ring.rawValue
+                    existingEntry.primary = contactTrickEntry.primary.rawValue
+                    existingEntry.top = contactTrickEntry.top.rawValue
+                    existingEntry.bottom = contactTrickEntry.bottom.rawValue
+                    existingEntry.hasHighContrast = contactTrickEntry.hasHighContrast
+                    existingEntry.ringWidth = Int16(contactTrickEntry.ringWidth.rawValue)
+                    existingEntry.ringGap = Int16(contactTrickEntry.ringGap.rawValue)
+                    existingEntry.fontSize = Int16(contactTrickEntry.fontSize.rawValue)
+                    existingEntry.fontSizeSecondary = Int16(contactTrickEntry.secondaryFontSize.rawValue)
+                    existingEntry.fontWeight = contactTrickEntry.fontWeight.asString
+                    existingEntry.fontWidth = contactTrickEntry.fontWidth.asString
+
+                    guard self.backgroundContext.hasChanges else { return }
+                    try self.backgroundContext.save()
+                } else {
+                    debugPrint(
+                        "\(DebuggingIdentifiers.failed) \(#file) \(#function) No matching Contact Trick Entry found to update."
+                    )
+                }
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update Contact Trick Entry with error: \(error.userInfo)"
+                )
+            }
+        }
+    }
+
+    /// Deletes a Contact Trick entry from Core Data.
+    ///
+    /// - Parameter objectID: The `NSManagedObjectID` of the object to delete.
+    func deleteContactTrickEntry(_ objectID: NSManagedObjectID) async {
+        await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
+    }
+}

+ 29 - 3
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -10,6 +10,7 @@ import Swinject
 protocol GlucoseStorage {
     var updatePublisher: AnyPublisher<Void, Never> { get }
     func storeGlucose(_ glucose: [BloodGlucose])
+    func addManualGlucose(glucose: Int)
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
@@ -173,6 +174,31 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         }
     }
 
+    func addManualGlucose(glucose: Int) {
+        coredataContext.perform {
+            let newItem = GlucoseStored(context: self.coredataContext)
+            newItem.id = UUID()
+            newItem.date = Date()
+            newItem.glucose = Int16(glucose)
+            newItem.isManual = true
+            newItem.isUploadedToNS = false
+            newItem.isUploadedToHealth = false
+            newItem.isUploadedToTidepool = false
+
+            do {
+                guard self.coredataContext.hasChanges else { return }
+                try self.coredataContext.save()
+
+                // Glucose subscribers already listen to the update publisher, so call here to update glucose-related data.
+                self.updateSubject.send(())
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save manual glucose to Core Data with error: \(error)"
+                )
+            }
+        }
+    }
+
     func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
         guard let glucoseDate = glucoseDate else { return false }
         return glucoseDate > Date().addingTimeInterval(-6 * 60)
@@ -238,14 +264,14 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
     func fetchLatestGlucose() -> GlucoseStored? {
         let predicate = NSPredicate.predicateFor20MinAgo
-        return CoreDataStack.shared.fetchEntities(
+        return (CoreDataStack.shared.fetchEntities(
             ofType: GlucoseStored.self,
             onContext: coredataContext,
             predicate: predicate,
             key: "date",
             ascending: false,
             fetchLimit: 1
-        ).first
+        ) as? [GlucoseStored] ?? []).first
     }
 
     // Fetch glucose that is not uploaded to Nightscout yet
@@ -303,7 +329,7 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
                     rate: nil,
                     eventType: .capillaryGlucose,
                     createdAt: result.date,
-                    enteredBy: CarbsEntry.manual,
+                    enteredBy: CarbsEntry.local,
                     bolus: nil,
                     insulin: nil,
                     notes: "Trio User",

+ 15 - 22
FreeAPS/Sources/APS/Storage/OverrideStorage.swift

@@ -15,7 +15,7 @@ protocol OverrideStorage {
     func getPresetOverridesForNightscout() async -> [NightscoutPresetOverride]
 }
 
-final class BaseOverrideStorage: OverrideStorage, Injectable {
+final class BaseOverrideStorage: @preconcurrency OverrideStorage, Injectable {
     @Injected() private var settingsManager: SettingsManager!
 
     private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
@@ -89,7 +89,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
 
     @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
         guard let overrideTarget = override.target, overrideTarget != 0 else {
-            return 100 // default
+            return 0
         }
         return overrideTarget.decimalValue
     }
@@ -125,38 +125,31 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
             newOverride.duration = override.duration as NSDecimalNumber
             newOverride.indefinite = override.indefinite
             newOverride.percentage = override.percentage
+            newOverride.isfAndCr = override.isfAndCr
+            newOverride.isf = override.isf
+            newOverride.cr = override.cr
             newOverride.enabled = override.enabled
             newOverride.smbIsOff = override.smbIsOff
             if override.overrideTarget {
-                newOverride.target = (
-                    self.settingsManager.settings.units == .mmolL ? override.target.asMgdL : override.target
-                ) as NSDecimalNumber
+                newOverride.target = override.target as NSDecimalNumber
             } else {
                 newOverride.target = 0
             }
             if override.advancedSettings {
                 newOverride.advancedSettings = true
 
-                if !override.isfAndCr {
-                    newOverride.isfAndCr = false
-                    newOverride.isf = override.isf
-                    newOverride.cr = override.cr
-                } else {
-                    newOverride.isfAndCr = true
-                }
-
-                if override.smbIsAlwaysOff {
-                    newOverride.smbIsAlwaysOff = true
-                    newOverride.start = override.start as NSDecimalNumber
-                    newOverride.end = override.end as NSDecimalNumber
-                } else {
-                    newOverride.smbIsAlwaysOff = false
-                }
-
                 newOverride.smbMinutes = override.smbMinutes as NSDecimalNumber
                 newOverride.uamMinutes = override.uamMinutes as NSDecimalNumber
             }
 
+            if override.smbIsScheduledOff {
+                newOverride.smbIsScheduledOff = true
+                newOverride.start = override.start as NSDecimalNumber
+                newOverride.end = override.end as NSDecimalNumber
+            } else {
+                newOverride.smbIsScheduledOff = false
+            }
+
             do {
                 guard self.backgroundContext.hasChanges else { return }
                 try self.backgroundContext.save()
@@ -185,7 +178,7 @@ final class BaseOverrideStorage: OverrideStorage, Injectable {
         newOverride.isfAndCr = override.isfAndCr
         newOverride.isf = override.isf
         newOverride.cr = override.cr
-        newOverride.smbIsAlwaysOff = override.smbIsAlwaysOff
+        newOverride.smbIsScheduledOff = override.smbIsScheduledOff
         newOverride.start = override.start
         newOverride.end = override.end
         newOverride.smbMinutes = override.smbMinutes

+ 1 - 1
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -60,7 +60,7 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         key: "timestamp",
                         ascending: false,
                         batchSize: 50
-                    )
+                    ) as? [PumpEventStored] ?? []
 
                     switch event.type {
                     case .bolus:

+ 266 - 50
FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift

@@ -1,3 +1,4 @@
+import CoreData
 import Foundation
 import SwiftDate
 import Swinject
@@ -7,59 +8,218 @@ protocol TempTargetsObserver {
 }
 
 protocol TempTargetsStorage {
-    func storeTempTargets(_ targets: [TempTarget])
+    func storeTempTarget(tempTarget: TempTarget) async
+    func saveTempTargetsToStorage(_ targets: [TempTarget])
+    func fetchForTempTargetPresets() async -> [NSManagedObjectID]
+    func fetchScheduledTempTargets() async -> [NSManagedObjectID]
+    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID]
+    func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID
+    func deleteOverridePreset(_ objectID: NSManagedObjectID) async
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID]
     func syncDate() -> Date
     func recent() -> [TempTarget]
-    func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment]
-    func storePresets(_ targets: [TempTarget])
+    func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
+    func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment]
     func presets() -> [TempTarget]
     func current() -> TempTarget?
+    func existsTempTarget(with date: Date) async -> Bool
 }
 
 final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
     private let processQueue = DispatchQueue(label: "BaseTempTargetsStorage.processQueue")
     @Injected() private var storage: FileStorage!
     @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var settingsManager: SettingsManager!
+
+    private let backgroundContext = CoreDataStack.shared.newTaskContext()
+    private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
     init(resolver: Resolver) {
         injectServices(resolver)
     }
 
-    func storeTempTargets(_ targets: [TempTarget]) {
-        storeTempTargets(targets, isPresets: false)
+    func loadLatestTempTargetConfigurations(fetchLimit: Int) async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.lastActiveTempTarget,
+            key: "orderPosition",
+            ascending: true,
+            fetchLimit: fetchLimit
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await backgroundContext.perform {
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    /// Returns the NSManagedObjectID of the Temp Target Presets
+    func fetchForTempTargetPresets() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.allTempTargetPresets,
+            key: "orderPosition",
+            ascending: true
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await backgroundContext.perform {
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    func fetchScheduledTempTargets() async -> [NSManagedObjectID] {
+        let scheduledTempTargets = NSPredicate(format: "date > %@", Date() as NSDate)
+
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: scheduledTempTargets,
+            key: "date",
+            ascending: false
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await backgroundContext.perform {
+            return fetchedResults.map(\.objectID)
+        }
     }
 
-    private func storeTempTargets(_ targets: [TempTarget], isPresets: Bool) {
-        processQueue.sync {
-            var targets = targets
-            if !isPresets {
-                if current() != nil, let newActive = targets.last(where: {
-                    $0.createdAt.addingTimeInterval(Int($0.duration).minutes.timeInterval) > Date()
-                        && $0.createdAt <= Date()
-                }) {
-                    // cancel current
-                    targets += [TempTarget.cancel(at: newActive.createdAt.addingTimeInterval(-1))]
-                }
+    func fetchScheduledTempTarget(for targetDate: Date) async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "date == %@", targetDate as NSDate)
+
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: predicate,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        )
+
+        guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+
+        return await backgroundContext.perform {
+            fetchedResults.map(\.objectID)
+        }
+    }
+
+    func storeTempTarget(tempTarget: TempTarget) async {
+        var presetCount = -1
+        if tempTarget.isPreset == true {
+            let presets = await fetchForTempTargetPresets()
+            presetCount = presets.count
+        }
+
+        await backgroundContext.perform {
+            let newTempTarget = TempTargetStored(context: self.backgroundContext)
+            newTempTarget.date = tempTarget.createdAt
+            newTempTarget.id = UUID()
+            newTempTarget.enabled = tempTarget.enabled ?? false
+            newTempTarget.duration = tempTarget.duration as NSDecimalNumber
+            newTempTarget.isUploadedToNS = false
+            newTempTarget.name = tempTarget.name
+            newTempTarget.target = NSDecimalNumber(decimal: tempTarget.targetTop ?? 0)
+            newTempTarget.isPreset = tempTarget.isPreset ?? false
+
+            // Nullify half basal target to ensure the latest HBT is used via OpenAPS Manager when sending TT data to oref
+            newTempTarget.halfBasalTarget = nil
+
+            if let halfBasalTarget = tempTarget.halfBasalTarget,
+               halfBasalTarget != self.settingsManager.preferences.halfBasalExerciseTarget
+            {
+                newTempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalTarget)
+            }
+
+            if tempTarget.isPreset == true, presetCount > -1 {
+                newTempTarget.orderPosition = Int16(presetCount + 1)
+            }
+
+            do {
+                guard self.backgroundContext.hasChanges else { return }
+                try self.backgroundContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target to Core Data with error: \(error.userInfo)"
+                )
             }
+        }
+    }
 
-            let file = isPresets ? OpenAPS.FreeAPS.tempTargetsPresets : OpenAPS.Settings.tempTargets
+    func saveTempTargetsToStorage(_ targets: [TempTarget]) {
+        processQueue.async {
+            let file = OpenAPS.Settings.tempTargets
             var uniqEvents: [TempTarget] = []
             self.storage.transaction { storage in
                 storage.append(targets, to: file, uniqBy: \.createdAt)
-                uniqEvents = storage.retrieve(file, as: [TempTarget].self)?
-                    .filter {
-                        guard !isPresets else { return true }
-                        return $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date()
-                    }
-                    .sorted { $0.createdAt > $1.createdAt } ?? []
-                storage.save(Array(uniqEvents), as: file)
+
+                let retrievedTargets = storage.retrieve(file, as: [TempTarget].self) ?? []
+                uniqEvents = retrievedTargets
+                    .filter { $0.isWithinLastDay }
+                    .sorted(by: { $0.createdAt > $1.createdAt })
+
+                storage.save(uniqEvents, as: file)
             }
-            broadcaster.notify(TempTargetsObserver.self, on: processQueue) {
+
+            self.broadcaster.notify(TempTargetsObserver.self, on: self.processQueue) {
                 $0.tempTargetsDidUpdate(uniqEvents)
             }
         }
     }
 
+    func existsTempTarget(with date: Date) async -> Bool {
+        await backgroundContext.perform {
+            // Fetch all Temp Targets with the given date
+            let fetchRequest: NSFetchRequest<TempTargetStored> = TempTargetStored.fetchRequest()
+            fetchRequest.predicate = NSPredicate(format: "date == %@", date as NSDate)
+
+            do {
+                let results = try self.backgroundContext.fetch(fetchRequest)
+                return !results.isEmpty
+            } catch let error as NSError {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to check for existing Temp Target: \(error)")
+                return false
+            }
+        }
+    }
+
+    // Copy the current Temp Target if it is a RUNNING Preset
+    /// otherwise we would edit the Preset
+    @MainActor func copyRunningTempTarget(_ tempTarget: TempTargetStored) async -> NSManagedObjectID {
+        let newTempTarget = TempTargetStored(context: viewContext)
+        newTempTarget.date = tempTarget.date
+        newTempTarget.id = tempTarget.id
+        newTempTarget.enabled = tempTarget.enabled
+        newTempTarget.duration = tempTarget.duration
+        newTempTarget.isUploadedToNS = true // to avoid getting duplicates on NS
+        newTempTarget.name = tempTarget.name
+        newTempTarget.target = tempTarget.target
+        newTempTarget.isPreset = false // no Preset
+        newTempTarget.halfBasalTarget = tempTarget.halfBasalTarget != 160 ? tempTarget.halfBasalTarget : nil
+
+        await viewContext.perform {
+            do {
+                guard self.viewContext.hasChanges else { return }
+                try self.viewContext.save()
+            } catch let error as NSError {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to copy Temp Target with error: \(error.userInfo)"
+                )
+            }
+        }
+
+        return newTempTarget.objectID
+    }
+
+    @MainActor func deleteOverridePreset(_ objectID: NSManagedObjectID) async {
+        await CoreDataStack.shared.deleteObject(identifiedBy: objectID)
+    }
+
     func syncDate() -> Date {
         Date().addingTimeInterval(-1.days.timeInterval)
     }
@@ -82,38 +242,94 @@ final class BaseTempTargetsStorage: TempTargetsStorage, Injectable {
         return last
     }
 
-    func nightscoutTreatmentsNotUploaded() -> [NightscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedTempTargets, as: [NightscoutTreatment].self) ?? []
-
-        let eventsManual = recent().filter { $0.enteredBy == TempTarget.manual }
-        let treatments = eventsManual.map {
-            NightscoutTreatment(
-                duration: Int($0.duration),
-                rawDuration: nil,
-                rawRate: nil,
-                absolute: nil,
-                rate: nil,
-                eventType: .nsTempTarget,
-                createdAt: $0.createdAt,
-                enteredBy: TempTarget.manual,
-                bolus: nil,
-                insulin: nil,
-                notes: nil,
-                carbs: nil,
-                targetTop: $0.targetTop,
-                targetBottom: $0.targetBottom
-            )
+    func getTempTargetsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate.lastActiveOverrideNotYetUploadedToNightscout, // TODO: create adjustment predicate (OR+TT)
+            key: "date",
+            ascending: false
+        )
+
+        return await backgroundContext.perform {
+            guard let fetchedTempTargets = results as? [TempTargetStored] else { return [] }
+
+            return fetchedTempTargets.map { tempTarget in
+                NightscoutTreatment(
+                    duration: Int(truncating: tempTarget.duration ?? 60),
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsTempTarget,
+                    createdAt: tempTarget.date ?? Date(),
+                    enteredBy: TempTarget.local,
+                    bolus: nil,
+                    insulin: nil,
+                    notes: tempTarget.name ?? TempTarget.custom,
+                    carbs: nil,
+                    targetTop: tempTarget
+                        .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL),
+                    targetBottom: tempTarget
+                        .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL)
+                )
+            }
         }
-        return Array(Set(treatments).subtracting(Set(uploaded)))
     }
 
-    func storePresets(_ targets: [TempTarget]) {
-        storage.remove(OpenAPS.FreeAPS.tempTargetsPresets)
+    func getTempTargetRunsNotYetUploadedToNightscout() async -> [NightscoutTreatment] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetRunStored.self,
+            onContext: backgroundContext,
+            predicate: NSPredicate(
+                format: "startDate >= %@ AND isUploadedToNS == %@",
+                Date.oneDayAgo as NSDate,
+                false as NSNumber
+            ),
+            key: "startDate",
+            ascending: false
+        )
+
+        return await backgroundContext.perform {
+            guard let fetchedTempTargetRuns = results as? [TempTargetRunStored] else { return [] }
 
-        storeTempTargets(targets, isPresets: true)
+            return fetchedTempTargetRuns.map { tempTargetRun in
+                var durationInMinutes = (tempTargetRun.endDate?.timeIntervalSince(tempTargetRun.startDate ?? Date()) ?? 1) / 60
+                durationInMinutes = durationInMinutes < 1 ? 1 : durationInMinutes
+                return NightscoutTreatment(
+                    duration: Int(durationInMinutes),
+                    rawDuration: nil,
+                    rawRate: nil,
+                    absolute: nil,
+                    rate: nil,
+                    eventType: .nsTempTarget,
+                    createdAt: (tempTargetRun.startDate ?? tempTargetRun.tempTarget?.date) ?? Date(),
+                    enteredBy: TempTarget.local,
+                    bolus: nil,
+                    insulin: nil,
+                    notes: tempTargetRun.tempTarget?.name ?? TempTarget.custom,
+                    carbs: nil,
+                    targetTop: tempTargetRun
+                        .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL),
+                    targetBottom: tempTargetRun
+                        .target as Decimal? ?? (self.settingsManager.settings.units == .mgdL ? 100.0 : 100.asMmolL)
+                )
+            }
+        }
     }
 
     func presets() -> [TempTarget] {
         storage.retrieve(OpenAPS.FreeAPS.tempTargetsPresets, as: [TempTarget].self)?.reversed() ?? []
     }
 }
+
+private extension TempTarget {
+    var isActive: Bool {
+        let expirationTime = createdAt.addingTimeInterval(Int(duration).minutes.timeInterval)
+        return expirationTime > Date() && createdAt <= Date()
+    }
+
+    var isWithinLastDay: Bool {
+        createdAt.addingTimeInterval(1.days.timeInterval) > Date()
+    }
+}

+ 20 - 0
FreeAPS/Sources/Application/AppState.swift

@@ -0,0 +1,20 @@
+import Foundation
+import Observation
+import SwiftUICore
+import UIKit
+
+@Observable class AppState {
+    func trioBackgroundColor(for colorScheme: ColorScheme) -> 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
+            )
+    }
+}

+ 21 - 3
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -15,6 +15,8 @@ import Swinject
 
     let coreDataStack = CoreDataStack.shared
 
+    @State private var appState = AppState()
+
     // Dependencies Assembler
     // contain all dependencies Assemblies
     // TODO: Remove static key after update "Use Dependencies" logic
@@ -60,10 +62,15 @@ import Swinject
     init() {
         debug(
             .default,
-            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(BuildDetails.default.buildDate())] [buildExpires: \(BuildDetails.default.calculateExpirationDate())]"
+            "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))]"
         )
+
+        // Load services
         loadServices()
 
+        // Fix bug in iOS 18 related to the translucent tab bar
+        configureTabBarAppearance()
+
         // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
         cleanupOldData()
     }
@@ -71,12 +78,13 @@ import Swinject
     var body: some Scene {
         WindowGroup {
             Main.RootView(resolver: resolver)
-                .preferredColorScheme(colorScheme(for: colorSchemePreference ?? .systemDefault) ?? nil)
+                .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
                 .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
+                .environment(appState)
                 .environmentObject(Icons())
                 .onOpenURL(perform: handleURL)
         }
-        .onChange(of: scenePhase) { newScenePhase in
+        .onChange(of: scenePhase) { _, newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")
 
             /// If the App goes to the background we should ensure that all the changes are saved from the viewContext to the Persistent Container
@@ -90,6 +98,16 @@ import Swinject
         }
     }
 
+    func configureTabBarAppearance() {
+        let appearance = UITabBarAppearance()
+        appearance.configureWithDefaultBackground()
+        appearance.backgroundEffect = UIBlurEffect(style: .systemChromeMaterial)
+        appearance.backgroundColor = UIColor.clear
+
+        UITabBar.appearance().standardAppearance = appearance
+        UITabBar.appearance().scrollEdgeAppearance = appearance
+    }
+
     private func colorScheme(for colorScheme: ColorSchemeOption) -> ColorScheme? {
         switch colorScheme {
         case .systemDefault:

+ 1 - 0
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -20,6 +20,7 @@ final class ServiceAssembly: Assembly {
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
         container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) }
+        container.register(ContactTrickManager.self) { r in BaseContactTrickManager(resolver: r) }
         container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }
         if #available(iOS 16.2, *) {
             container.register(LiveActivityBridge.self) { r in

+ 1 - 0
FreeAPS/Sources/Assemblies/StorageAssembly.swift

@@ -13,6 +13,7 @@ final class StorageAssembly: Assembly {
         container.register(GlucoseStorage.self) { r in BaseGlucoseStorage(resolver: r) }
         container.register(TempTargetsStorage.self) { r in BaseTempTargetsStorage(resolver: r) }
         container.register(CarbsStorage.self) { r in BaseCarbsStorage(resolver: r) }
+        container.register(ContactTrickStorage.self) { r in BaseContactTrickStorage(resolver: r) }
         container.register(AnnouncementsStorage.self) { r in BaseAnnouncementsStorage(resolver: r) }
         container.register(SettingsManager.self) { r in BaseSettingsManager(resolver: r) }
         container.register(Keychain.self) { _ in BaseKeychain() }

+ 6 - 0
FreeAPS/Sources/Helpers/Decimal+Extensions.swift

@@ -13,6 +13,12 @@ extension Int {
     }
 }
 
+extension Int16 {
+    var minutes: TimeInterval {
+        TimeInterval(self) * 60
+    }
+}
+
 extension CGFloat {
     init(_ decimal: Decimal) {
         self.init(Double(decimal))

+ 51 - 0
FreeAPS/Sources/Helpers/Formatters.swift

@@ -28,6 +28,57 @@ extension Formatter {
         formatter.formatOptions = [.withInternetDateTime]
         return formatter
     }()
+
+    static let decimalFormatterWithTwoFractionDigits: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        return formatter
+    }()
+
+    static let dateFormatter: DateFormatter = {
+        let dateFormatter = DateFormatter()
+        dateFormatter.timeStyle = .short
+        return dateFormatter
+    }()
+
+    static let decimalFormatterWithOneFractionDigit: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }()
+
+    static let integerFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        return formatter
+    }()
+
+    static func glucoseFormatter(for units: GlucoseUnits) -> NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.roundingMode = .halfUp
+
+        switch units {
+        case .mmolL:
+            formatter.maximumFractionDigits = 1
+        case .mgdL:
+            formatter.maximumFractionDigits = 0
+        }
+
+        return formatter
+    }
+
+    static let bolusFormatter: NumberFormatter = {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimumIntegerDigits = 0
+        formatter.maximumFractionDigits = 2
+        formatter.decimalSeparator = "."
+        return formatter
+    }()
 }
 
 extension JSONDecoder.DateDecodingStrategy {

+ 138 - 28
FreeAPS/Sources/Helpers/MainChartHelper.swift

@@ -1,5 +1,7 @@
+import Charts
 import CoreData
 import Foundation
+import SwiftUICore
 
 enum MainChartHelper {
     // Calculates the glucose value thats the nearest to parameter 'time'
@@ -47,53 +49,161 @@ enum MainChartHelper {
         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? {
+    static func calculateDuration(
+        objectID: NSManagedObjectID,
+        attribute: String,
+        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
+            let object = try context.existingObject(with: objectID)
+            if let attributeValue = object.value(forKey: attribute) as? NSDecimalNumber {
+                let doubleValue = attributeValue.doubleValue
+                if doubleValue != 0 {
+                    return TimeInterval(doubleValue * 60) // return seconds
+                }
+            } else {
+                debugPrint("Attribute \(attribute) not found or not of type NSDecimalNumber")
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate duration for object with error: \(error.localizedDescription)"
             )
         }
+
         return nil
     }
 
-    static func calculateTarget(objectID: NSManagedObjectID, context: NSManagedObjectContext) -> Decimal? {
+    static func calculateTarget(objectID: NSManagedObjectID, attribute: String, context: NSManagedObjectContext) -> Decimal? {
         do {
-            if let override = try context.existingObject(with: objectID) as? OverrideStored,
-               let overrideTarget = override.target, overrideTarget != 0
-            {
-                return overrideTarget.decimalValue
+            let object = try context.existingObject(with: objectID)
+            if let attributeValue = object.value(forKey: attribute) as? NSDecimalNumber, attributeValue != 0 {
+                return attributeValue.decimalValue
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate Override Target with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to calculate target for object with error: \(error.localizedDescription)"
             )
         }
         return nil
     }
 }
+
+// MARK: - Rule Marks and Charts configurations
+
+extension MainChartView {
+    func drawCurrentTimeMarker() -> some ChartContent {
+        RuleMark(
+            x: .value(
+                "",
+                Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
+                unit: .second
+            )
+        ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
+    }
+
+    func drawStartRuleMark() -> some ChartContent {
+        RuleMark(
+            x: .value(
+                "",
+                startMarker,
+                unit: .second
+            )
+        ).foregroundStyle(Color.clear)
+    }
+
+    func drawEndRuleMark() -> some ChartContent {
+        RuleMark(
+            x: .value(
+                "",
+                endMarker,
+                unit: .second
+            )
+        ).foregroundStyle(Color.clear)
+    }
+
+    func basalChartPlotStyle(_ plotContent: ChartPlotContent) -> some View {
+        plotContent
+            .rotationEffect(.degrees(180))
+            .scaleEffect(x: -1, y: 1)
+    }
+
+    var mainChartXAxis: some AxisContent {
+        AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
+            if displayXgridLines {
+                AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            } else {
+                AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+            }
+        }
+    }
+
+    var basalChartXAxis: some AxisContent {
+        AxisMarks(values: .stride(by: .hour, count: screenHours > 6 ? (screenHours > 12 ? 4 : 2) : 1)) { _ in
+            if displayXgridLines {
+                AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            } else {
+                AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+            }
+            AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                .font(.footnote).foregroundStyle(Color.primary)
+        }
+    }
+
+    var mainChartYAxis: some AxisContent {
+        AxisMarks(position: .trailing) { value in
+
+            if displayYgridLines {
+                AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            } else {
+                AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+            }
+
+            if let glucoseValue = value.as(Double.self), glucoseValue > 0 {
+                /// fix offset between the two charts...
+                if units == .mmolL {
+                    AxisTick(length: 7, stroke: .init(lineWidth: 7)).foregroundStyle(Color.clear)
+                }
+                AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
+            }
+        }
+    }
+
+    var cobIobChartYAxis: some AxisContent {
+        AxisMarks(position: .trailing) { _ in
+            if displayYgridLines {
+                AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            } else {
+                AxisGridLine(stroke: .init(lineWidth: 0, dash: [2, 3]))
+            }
+        }
+    }
+}
+
+// MARK: - Calculations and formatting
+
+extension MainChartView {
+    func fullWidth(viewWidth: CGFloat) -> CGFloat {
+        viewWidth * CGFloat(hours) / CGFloat(min(max(screenHours, 2), 24))
+    }
+
+    // Update start and  end marker to fix scroll update problem with x axis
+    func updateStartEndMarkers() {
+        startMarker = Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970 - 86400))
+
+        let threeHourSinceNow = Date(timeIntervalSinceNow: TimeInterval(hours: 3))
+
+        // min is 1.5h -> (1.5*1h = 1.5*(5*12*60))
+        let dynamicFutureDateForCone = Date(timeIntervalSinceNow: TimeInterval(
+            Int(1.5) * 5 * state
+                .minCount * 60
+        ))
+
+        endMarker = state
+            .forecastDisplayType == .lines ? threeHourSinceNow : dynamicFutureDateForCone <= threeHourSinceNow ?
+            dynamicFutureDateForCone.addingTimeInterval(TimeInterval(minutes: 30)) : threeHourSinceNow
+    }
+}

+ 1 - 1
FreeAPS/Sources/Helpers/ProgressBar.swift

@@ -17,7 +17,7 @@ struct ProgressBar: View {
                         height: geometry.size.height
                     )
                     .foregroundColor(.accentColor)
-                    .animation(.linear)
+                    .animation(.linear, value: value)
             }
         }
         .frame(height: 20)

+ 1 - 1
FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Enact Temp Target";
+"Start Temp Target" = "Start Temp Target";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings

@@ -110,7 +110,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Enact Temp Target";
+"Start Temp Target" = "Start Temp Target";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Varighed";
 
 /*  */
-"Enact Temp Target" = "Udfør Midlertidigt Mål";
+"Start Temp Target" = "Udfør Midlertidigt Mål";
 
 /* */
 "Target" = "Mål";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings

@@ -158,7 +158,7 @@
 "Duration" = "Dauer";
 
 /*  */
-"Enact Temp Target" = "Temporäres Ziel starten";
+"Start Temp Target" = "Temporäres Ziel starten";
 
 /* */
 "Target" = "Ziel";

+ 4 - 4
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Enact Temp Target";
+"Start Temp Target" = "Start Temp Target";
 
 /* */
 "Target" = "Target";
@@ -1859,10 +1859,10 @@ Enact a temp Basal or a temp target */
 /* Smoothing of CGM readings */
 "Smooth Glucose Value" = "Smooth Glucose Value";
 
- /* -----------------------------------------------------------------------------------------------------------
+/* -----------------------------------------------------------------------------------------------------------
 
-  Infotexts from openaps.docs and androidaps.docs
-  Trio
+ Infotexts from openaps.docs and androidaps.docs
+ Trio
 */
 
 /* Headline Rewind Resets Autosens */

+ 1 - 1
FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duración";
 
 /*  */
-"Enact Temp Target" = "Iniciar objetivo temporal";
+"Start Temp Target" = "Iniciar objetivo temporal";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings

@@ -161,7 +161,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Enact Temp Target";
+"Start Temp Target" = "Start Temp Target";
 
 /* */
 "Target" = "Tavoite";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Durée";
 
 /*  */
-"Enact Temp Target" = "Activer la cible temporaire";
+"Start Temp Target" = "Activer la cible temporaire";
 
 /* */
 "Target" = "Cible";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Enact Temp Target";
+"Start Temp Target" = "Start Temp Target";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Átmeneti cél bekapcsolása";
+"Start Temp Target" = "Átmeneti cél bekapcsolása";
 
 /* */
 "Target" = "Cél";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Durata";
 
 /*  */
-"Enact Temp Target" = "Target Temporaneo";
+"Start Temp Target" = "Target Temporaneo";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Varighet";
 
 /*  */
-"Enact Temp Target" = "Start midlertidig mål";
+"Start Temp Target" = "Start midlertidig mål";
 
 /* */
 "Target" = "Mål";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duur";
 
 /*  */
-"Enact Temp Target" = "Start tijdelijk doel";
+"Start Temp Target" = "Start tijdelijk doel";
 
 /* */
 "Target" = "Doelbereik";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Czas trwania";
 
 /*  */
-"Enact Temp Target" = "Enact Temp Target";
+"Start Temp Target" = "Start Temp Target";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duração";
 
 /*  */
-"Enact Temp Target" = "Executar Meta Temporária";
+"Start Temp Target" = "Executar Meta Temporária";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duração";
 
 /*  */
-"Enact Temp Target" = "Executar Meta Temporária";
+"Start Temp Target" = "Executar Meta Temporária";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Длительность";
 
 /*  */
-"Enact Temp Target" = "Временная цель";
+"Start Temp Target" = "Временная цель";
 
 /* */
 "Target" = "Цель";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Trvanie";
 
 /*  */
-"Enact Temp Target" = "Nastaviť dočasný cieľ";
+"Start Temp Target" = "Nastaviť dočasný cieľ";
 
 /* */
 "Target" = "Cieľ";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Tillfälliga målvärden";
+"Start Temp Target" = "Tillfälliga målvärden";
 
 /* */
 "Target" = "Målvärde";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Süre";
 
 /*  */
-"Enact Temp Target" = "Geçici Hedefe Başla";
+"Start Temp Target" = "Geçici Hedefe Başla";
 
 /* */
 "Target" = "Target";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Тривалість";
 
 /*  */
-"Enact Temp Target" = "Запустити Тимчасову ціль";
+"Start Temp Target" = "Запустити Тимчасову ціль";
 
 /* */
 "Target" = "Ціль";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "Duration";
 
 /*  */
-"Enact Temp Target" = "Chấp nhận mục tiêu tạm thời";
+"Start Temp Target" = "Chấp nhận mục tiêu tạm thời";
 
 /* */
 "Target" = "Mục tiêu";

+ 1 - 1
FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings

@@ -155,7 +155,7 @@
 "Duration" = "持续时间";
 
 /*  */
-"Enact Temp Target" = "设置临时目标";
+"Start Temp Target" = "设置临时目标";
 
 /* */
 "Target" = "Target";

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

@@ -15,7 +15,7 @@ struct AlertEntry: JSON, Codable, Hashable {
     let contentBody: String?
     var errorMessage: String?
 
-    static let manual = "Trio"
+    static let local = "Trio"
 
     static func == (lhs: AlertEntry, rhs: AlertEntry) -> Bool {
         lhs.issuedDate == rhs.issuedDate

+ 2 - 0
FreeAPS/Sources/Models/Battery.swift

@@ -10,4 +10,6 @@ struct Battery: JSON {
 enum BatteryState: String, JSON {
     case normal
     case low
+    case unknown
+    case error
 }

+ 2 - 2
FreeAPS/Sources/Models/CarbsEntry.swift

@@ -13,7 +13,7 @@ struct CarbsEntry: JSON, Equatable, Hashable, Identifiable {
     let isFPU: Bool?
     let fpuID: String?
 
-    static let manual = "Trio"
+    static let local = "Trio"
     static let appleHealth = "applehealth"
 
     static func == (lhs: CarbsEntry, rhs: CarbsEntry) -> Bool {
@@ -33,7 +33,7 @@ extension CarbsEntry {
         case carbs
         case fat
         case protein
-        case note
+        case note = "notes"
         case enteredBy
         case isFPU
         case fpuID

+ 192 - 0
FreeAPS/Sources/Models/ContactTrickEntry.swift

@@ -0,0 +1,192 @@
+import CoreData
+import SwiftUI
+
+struct ContactTrickEntry: Hashable, Equatable, Sendable {
+    var id = UUID()
+    var name: String = ""
+    var layout: ContactTrickLayout = .single
+    var ring: ContactTrickLargeRing = .none
+    var primary: ContactTrickValue = .glucose
+    var top: ContactTrickValue = .none
+    var bottom: ContactTrickValue = .none
+    var contactId: String? = nil
+    var hasHighContrast: Bool = true
+    var ringWidth: RingWidth = .regular
+    var ringGap: RingGap = .small
+    var fontSize: FontSize = .regular
+    var secondaryFontSize: FontSize = .small
+    var fontWeight: Font.Weight = .medium
+    var fontWidth: Font.Width = .standard
+    var managedObjectID: NSManagedObjectID?
+
+    static func == (lhs: ContactTrickEntry, rhs: ContactTrickEntry) -> Bool {
+        lhs.id == rhs.id &&
+            lhs.name == rhs.name &&
+            lhs.layout == rhs.layout &&
+            lhs.ring == rhs.ring &&
+            lhs.primary == rhs.primary &&
+            lhs.top == rhs.top &&
+            lhs.bottom == rhs.bottom &&
+            lhs.contactId == rhs.contactId &&
+            lhs.hasHighContrast == rhs.hasHighContrast &&
+            lhs.ringWidth == rhs.ringWidth &&
+            lhs.ringGap == rhs.ringGap &&
+            lhs.fontSize == rhs.fontSize &&
+            lhs.secondaryFontSize == rhs.secondaryFontSize &&
+            lhs.fontWeight == rhs.fontWeight &&
+            lhs.fontWidth == rhs.fontWidth
+    }
+
+    // Convert `fontWeight` to a String for Core Data storage
+    var fontWeightString: String {
+        fontWeight.asString
+    }
+
+    // Initialize `fontWeight` from a String
+    static func fontWeight(from string: String) -> Font.Weight {
+        Font.Weight.fromString(string)
+    }
+
+    // Convert `fontWidth` to a String for Core Data storage
+    var fontWidthString: String {
+        fontWidth.asString
+    }
+
+    // Initialize `fontWidth` from a String
+    static func fontWidth(from string: String) -> Font.Width {
+        Font.Width.fromString(string)
+    }
+
+    enum FontSize: Int, Codable, Sendable, CaseIterable {
+        case tiny = 200
+        case small = 250
+        case regular = 300
+        case large = 400
+
+        var displayName: String {
+            switch self {
+            case .tiny: return "Tiny"
+            case .small: return "Small"
+            case .regular: return "Regular"
+            case .large: return "Large"
+            }
+        }
+    }
+
+    enum RingWidth: Int, Codable, Sendable, CaseIterable {
+        case tiny = 3
+        case small = 5
+        case regular = 7
+        case medium = 10
+        case large = 15
+
+        var displayName: String {
+            switch self {
+            case .tiny: return "Tiny"
+            case .small: return "Small"
+            case .regular: return "Regular"
+            case .medium: return "Medium"
+            case .large: return "Large"
+            }
+        }
+    }
+
+    enum RingGap: Int, Codable, Sendable, CaseIterable {
+        case tiny = 1
+        case small = 2
+        case regular = 3
+        case medium = 4
+        case large = 5
+
+        var displayName: String {
+            switch self {
+            case .tiny: return "Tiny"
+            case .small: return "Small"
+            case .regular: return "Regular"
+            case .medium: return "Medium"
+            case .large: return "Large"
+            }
+        }
+    }
+}
+
+protocol ContactTrickObserver: Sendable {
+    // TODO: is this required?
+//    func basalProfileDidChange(_ entry: [ContactTrickEntry])
+}
+
+enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable {
+    var id: String { rawValue }
+    case none
+    case glucose
+    case eventualBG
+    case delta
+    case trend
+    case lastLoopDate
+    case cob
+    case iob
+    case ring
+
+    var displayName: String {
+        switch self {
+        case .none:
+            return NSLocalizedString("None", comment: "")
+        case .glucose:
+            return NSLocalizedString("Glucose Reading", comment: "")
+        case .eventualBG:
+            return NSLocalizedString("Eventual Glucose", comment: "")
+        case .delta:
+            return NSLocalizedString("Glucose Delta", comment: "")
+        case .trend:
+            return NSLocalizedString("Glucose Trend", comment: "")
+        case .lastLoopDate:
+            return NSLocalizedString("Last Loop Time", comment: "")
+        case .cob:
+            return NSLocalizedString("COB", comment: "")
+        case .iob:
+            return NSLocalizedString("IOB", comment: "")
+        case .ring:
+            return NSLocalizedString("Loop Status", comment: "")
+        }
+    }
+}
+
+enum ContactTrickLayout: String, JSON, CaseIterable, Identifiable, Codable {
+    var id: String { rawValue }
+    case single
+    case split
+
+    var displayName: String {
+        switch self {
+        case .single:
+            return NSLocalizedString("Single", comment: "")
+        case .split:
+            return NSLocalizedString("Split", comment: "")
+        }
+    }
+}
+
+enum ContactTrickLargeRing: String, JSON, CaseIterable, Identifiable, Codable {
+    // TODO: revisit rings for iob, cob and combined iob+cob with more user feedback
+    var id: String { rawValue }
+    case none
+    case loop
+//    case iob
+//    case cob
+//    case iobcob
+
+    var displayName: String {
+        switch self {
+        case .none:
+            return NSLocalizedString("Hidden", comment: "")
+        case .loop:
+            return NSLocalizedString("Loop Status", comment: "")
+//        case .iob:
+//            return NSLocalizedString("Insulin on Board (IOB)", comment: "")
+//        case .cob:
+//            return NSLocalizedString("Carbs on Board (COB)", comment: "")
+//        case .iobcob:
+//            return NSLocalizedString("IOB + COB", comment: "")
+        }
+    }
+}

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

@@ -71,9 +71,9 @@ struct DecimalPickerSettings {
     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,
+        step: 5,
         min: 100,
-        max: 200,
+        max: 300,
         type: PickerSetting.PickerSettingType.glucose
     )
     var maxCOB = PickerSetting(value: 120, step: 5, min: 0, max: 300, type: PickerSetting.PickerSettingType.gramms)

+ 1 - 0
FreeAPS/Sources/Models/NightscoutStatus.swift

@@ -24,6 +24,7 @@ struct NSPumpStatus: JSON {
 struct Uploader: JSON {
     let batteryVoltage: Decimal?
     let battery: Int
+    let isCharging: Bool?
 }
 
 struct NightscoutTimevalue: JSON {

+ 12 - 12
FreeAPS/Sources/Models/Oref2_variables.swift

@@ -5,19 +5,19 @@ struct Oref2_variables: JSON, Equatable {
     var weightedAverage: Decimal
     var past2hoursAverage: Decimal
     var date: Date
-    var isEnabled: Bool
-    var presetActive: Bool
     var overridePercentage: Decimal
     var useOverride: Bool
     var duration: Decimal
     var unlimited: Bool
-    var hbt: Decimal
     var overrideTarget: Decimal
     var smbIsOff: Bool
     var advancedSettings: Bool
     var isfAndCr: Bool
     var isf: Bool
     var cr: Bool
+    var smbIsScheduledOff: Bool
+    var start: Decimal
+    var end: Decimal
     var smbMinutes: Decimal
     var uamMinutes: Decimal
 
@@ -26,19 +26,19 @@ struct Oref2_variables: JSON, Equatable {
         weightedAverage: Decimal,
         past2hoursAverage: Decimal,
         date: Date,
-        isEnabled: Bool,
-        presetActive: Bool,
         overridePercentage: Decimal,
         useOverride: Bool,
         duration: Decimal,
         unlimited: Bool,
-        hbt: Decimal,
         overrideTarget: Decimal,
         smbIsOff: Bool,
         advancedSettings: Bool,
         isfAndCr: Bool,
         isf: Bool,
         cr: Bool,
+        smbIsScheduledOff: Bool,
+        start: Decimal,
+        end: Decimal,
         smbMinutes: Decimal,
         uamMinutes: Decimal
     ) {
@@ -46,19 +46,19 @@ struct Oref2_variables: JSON, Equatable {
         self.weightedAverage = weightedAverage
         self.past2hoursAverage = past2hoursAverage
         self.date = date
-        self.isEnabled = isEnabled
-        self.presetActive = presetActive
         self.overridePercentage = overridePercentage
         self.useOverride = useOverride
         self.duration = duration
         self.unlimited = unlimited
-        self.hbt = hbt
         self.overrideTarget = overrideTarget
         self.smbIsOff = smbIsOff
         self.advancedSettings = advancedSettings
         self.isfAndCr = isfAndCr
         self.isf = isf
         self.cr = cr
+        self.smbIsScheduledOff = smbIsScheduledOff
+        self.start = start
+        self.end = end
         self.smbMinutes = smbMinutes
         self.uamMinutes = uamMinutes
     }
@@ -70,19 +70,19 @@ extension Oref2_variables {
         case weightedAverage
         case past2hoursAverage
         case date
-        case isEnabled
-        case presetActive
         case overridePercentage
         case useOverride
         case duration
         case unlimited
-        case hbt
         case overrideTarget
         case smbIsOff
         case advancedSettings
         case isfAndCr
         case isf
         case cr
+        case smbIsScheduledOff
+        case start
+        case end
         case smbMinutes
         case uamMinutes
     }

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

@@ -16,7 +16,7 @@ struct Override {
     let isfAndCr: Bool
     let isf: Bool
     let cr: Bool
-    let smbIsAlwaysOff: Bool
+    let smbIsScheduledOff: Bool
     let start: Decimal
     let end: Decimal
     let smbMinutes: Decimal

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

@@ -1,6 +1,6 @@
 import Foundation
 
-struct Preferences: JSON {
+struct Preferences: JSON, Equatable {
     var maxIOB: Decimal = 0
     var maxDailySafetyMultiplier: Decimal = 3
     var currentBasalSafetyMultiplier: Decimal = 4
@@ -120,3 +120,220 @@ enum InsulinCurve: String, JSON, Identifiable, CaseIterable {
 
     var id: InsulinCurve { self }
 }
+
+extension Preferences: Decodable {
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        var preferences = Preferences()
+
+        if let maxIOB = try? container.decode(Decimal.self, forKey: .maxIOB) {
+            preferences.maxIOB = maxIOB
+        }
+
+        if let maxDailySafetyMultiplier = try? container.decode(Decimal.self, forKey: .maxDailySafetyMultiplier) {
+            preferences.maxDailySafetyMultiplier = maxDailySafetyMultiplier
+        }
+
+        if let currentBasalSafetyMultiplier = try? container.decode(Decimal.self, forKey: .currentBasalSafetyMultiplier) {
+            preferences.currentBasalSafetyMultiplier = currentBasalSafetyMultiplier
+        }
+
+        if let autosensMax = try? container.decode(Decimal.self, forKey: .autosensMax) {
+            preferences.autosensMax = autosensMax
+        }
+
+        if let autosensMin = try? container.decode(Decimal.self, forKey: .autosensMin) {
+            preferences.autosensMin = autosensMin
+        }
+
+        if let smbDeliveryRatio = try? container.decode(Decimal.self, forKey: .smbDeliveryRatio) {
+            preferences.smbDeliveryRatio = smbDeliveryRatio
+        }
+
+        if let rewindResetsAutosens = try? container.decode(Bool.self, forKey: .rewindResetsAutosens) {
+            preferences.rewindResetsAutosens = rewindResetsAutosens
+        }
+
+        if let highTemptargetRaisesSensitivity = try? container.decode(Bool.self, forKey: .highTemptargetRaisesSensitivity) {
+            preferences.highTemptargetRaisesSensitivity = highTemptargetRaisesSensitivity
+        }
+
+        if let lowTemptargetLowersSensitivity = try? container.decode(Bool.self, forKey: .lowTemptargetLowersSensitivity) {
+            preferences.lowTemptargetLowersSensitivity = lowTemptargetLowersSensitivity
+        }
+
+        if let sensitivityRaisesTarget = try? container.decode(Bool.self, forKey: .sensitivityRaisesTarget) {
+            preferences.sensitivityRaisesTarget = sensitivityRaisesTarget
+        }
+
+        if let resistanceLowersTarget = try? container.decode(Bool.self, forKey: .resistanceLowersTarget) {
+            preferences.resistanceLowersTarget = resistanceLowersTarget
+        }
+
+        if let advTargetAdjustments = try? container.decode(Bool.self, forKey: .advTargetAdjustments) {
+            preferences.advTargetAdjustments = advTargetAdjustments
+        }
+
+        if let exerciseMode = try? container.decode(Bool.self, forKey: .exerciseMode) {
+            preferences.exerciseMode = exerciseMode
+        }
+
+        if let halfBasalExerciseTarget = try? container.decode(Decimal.self, forKey: .halfBasalExerciseTarget) {
+            preferences.halfBasalExerciseTarget = halfBasalExerciseTarget
+        }
+
+        if let maxCOB = try? container.decode(Decimal.self, forKey: .maxCOB) {
+            preferences.maxCOB = maxCOB
+        }
+
+        if let wideBGTargetRange = try? container.decode(Bool.self, forKey: .wideBGTargetRange) {
+            preferences.wideBGTargetRange = wideBGTargetRange
+        }
+
+        if let skipNeutralTemps = try? container.decode(Bool.self, forKey: .skipNeutralTemps) {
+            preferences.skipNeutralTemps = skipNeutralTemps
+        }
+
+        if let unsuspendIfNoTemp = try? container.decode(Bool.self, forKey: .unsuspendIfNoTemp) {
+            preferences.unsuspendIfNoTemp = unsuspendIfNoTemp
+        }
+
+        if let min5mCarbimpact = try? container.decode(Decimal.self, forKey: .min5mCarbimpact) {
+            preferences.min5mCarbimpact = min5mCarbimpact
+        }
+
+        if let autotuneISFAdjustmentFraction = try? container.decode(Decimal.self, forKey: .autotuneISFAdjustmentFraction) {
+            preferences.autotuneISFAdjustmentFraction = autotuneISFAdjustmentFraction
+        }
+
+        if let remainingCarbsFraction = try? container.decode(Decimal.self, forKey: .remainingCarbsFraction) {
+            preferences.remainingCarbsFraction = remainingCarbsFraction
+        }
+
+        if let remainingCarbsCap = try? container.decode(Decimal.self, forKey: .remainingCarbsCap) {
+            preferences.remainingCarbsCap = remainingCarbsCap
+        }
+
+        if let enableUAM = try? container.decode(Bool.self, forKey: .enableUAM) {
+            preferences.enableUAM = enableUAM
+        }
+
+        if let a52RiskEnable = try? container.decode(Bool.self, forKey: .a52RiskEnable) {
+            preferences.a52RiskEnable = a52RiskEnable
+        }
+
+        if let enableSMBWithCOB = try? container.decode(Bool.self, forKey: .enableSMBWithCOB) {
+            preferences.enableSMBWithCOB = enableSMBWithCOB
+        }
+
+        if let enableSMBWithTemptarget = try? container.decode(Bool.self, forKey: .enableSMBWithTemptarget) {
+            preferences.enableSMBWithTemptarget = enableSMBWithTemptarget
+        }
+
+        if let enableSMBAlways = try? container.decode(Bool.self, forKey: .enableSMBAlways) {
+            preferences.enableSMBAlways = enableSMBAlways
+        }
+
+        if let enableSMBAfterCarbs = try? container.decode(Bool.self, forKey: .enableSMBAfterCarbs) {
+            preferences.enableSMBAfterCarbs = enableSMBAfterCarbs
+        }
+
+        if let allowSMBWithHighTemptarget = try? container.decode(Bool.self, forKey: .allowSMBWithHighTemptarget) {
+            preferences.allowSMBWithHighTemptarget = allowSMBWithHighTemptarget
+        }
+
+        if let maxSMBBasalMinutes = try? container.decode(Decimal.self, forKey: .maxSMBBasalMinutes) {
+            preferences.maxSMBBasalMinutes = maxSMBBasalMinutes
+        }
+
+        if let maxUAMSMBBasalMinutes = try? container.decode(Decimal.self, forKey: .maxUAMSMBBasalMinutes) {
+            preferences.maxUAMSMBBasalMinutes = maxUAMSMBBasalMinutes
+        }
+
+        if let smbInterval = try? container.decode(Decimal.self, forKey: .smbInterval) {
+            preferences.smbInterval = smbInterval
+        }
+
+        if let bolusIncrement = try? container.decode(Decimal.self, forKey: .bolusIncrement) {
+            preferences.bolusIncrement = bolusIncrement
+        }
+
+        if let curve = try? container.decode(InsulinCurve.self, forKey: .curve) {
+            preferences.curve = curve
+        }
+
+        if let useCustomPeakTime = try? container.decode(Bool.self, forKey: .useCustomPeakTime) {
+            preferences.useCustomPeakTime = useCustomPeakTime
+        }
+
+        if let insulinPeakTime = try? container.decode(Decimal.self, forKey: .insulinPeakTime) {
+            preferences.insulinPeakTime = insulinPeakTime
+        }
+
+        if let carbsReqThreshold = try? container.decode(Decimal.self, forKey: .carbsReqThreshold) {
+            preferences.carbsReqThreshold = carbsReqThreshold
+        }
+
+        if let noisyCGMTargetMultiplier = try? container.decode(Decimal.self, forKey: .noisyCGMTargetMultiplier) {
+            preferences.noisyCGMTargetMultiplier = noisyCGMTargetMultiplier
+        }
+
+        if let suspendZerosIOB = try? container.decode(Bool.self, forKey: .suspendZerosIOB) {
+            preferences.suspendZerosIOB = suspendZerosIOB
+        }
+
+        if let maxDeltaBGthreshold = try? container.decode(Decimal.self, forKey: .maxDeltaBGthreshold) {
+            preferences.maxDeltaBGthreshold = maxDeltaBGthreshold
+        }
+
+        if let adjustmentFactor = try? container.decode(Decimal.self, forKey: .adjustmentFactor) {
+            preferences.adjustmentFactor = adjustmentFactor
+        }
+
+        if let adjustmentFactorSigmoid = try? container.decode(Decimal.self, forKey: .adjustmentFactorSigmoid) {
+            preferences.adjustmentFactorSigmoid = adjustmentFactorSigmoid
+        }
+
+        if let sigmoid = try? container.decode(Bool.self, forKey: .sigmoid) {
+            preferences.sigmoid = sigmoid
+        }
+
+        if let enableDynamicCR = try? container.decode(Bool.self, forKey: .enableDynamicCR) {
+            preferences.enableDynamicCR = enableDynamicCR
+        }
+
+        if let useNewFormula = try? container.decode(Bool.self, forKey: .useNewFormula) {
+            preferences.useNewFormula = useNewFormula
+        }
+
+        if let useWeightedAverage = try? container.decode(Bool.self, forKey: .useWeightedAverage) {
+            preferences.useWeightedAverage = useWeightedAverage
+        }
+
+        if let weightPercentage = try? container.decode(Decimal.self, forKey: .weightPercentage) {
+            preferences.weightPercentage = weightPercentage
+        }
+
+        if let tddAdjBasal = try? container.decode(Bool.self, forKey: .tddAdjBasal) {
+            preferences.tddAdjBasal = tddAdjBasal
+        }
+
+        if let enableSMB_high_bg = try? container.decode(Bool.self, forKey: .enableSMB_high_bg) {
+            preferences.enableSMB_high_bg = enableSMB_high_bg
+        }
+
+        if let enableSMB_high_bg_target = try? container.decode(Decimal.self, forKey: .enableSMB_high_bg_target) {
+            preferences.enableSMB_high_bg_target = enableSMB_high_bg_target
+        }
+
+        if let threshold_setting = try? container.decode(Decimal.self, forKey: .threshold_setting) {
+            preferences.threshold_setting = threshold_setting
+        }
+
+        if let updateInterval = try? container.decode(Decimal.self, forKey: .updateInterval) {
+            preferences.updateInterval = updateInterval
+        }
+
+        self = preferences
+    }
+}

+ 13 - 4
FreeAPS/Sources/Models/TempTarget.swift

@@ -9,9 +9,12 @@ struct TempTarget: JSON, Identifiable, Equatable, Hashable {
     let duration: Decimal
     let enteredBy: String?
     let reason: String?
+    let isPreset: Bool?
+    var enabled: Bool?
+    let halfBasalTarget: Decimal?
 
-    static let manual = "Trio"
-    static let custom = "Temp target"
+    static let local = "Trio"
+    static let custom = "Temp Target"
     static let cancel = "Cancel"
 
     var displayName: String {
@@ -33,8 +36,11 @@ struct TempTarget: JSON, Identifiable, Equatable, Hashable {
             targetTop: 0,
             targetBottom: 0,
             duration: 0,
-            enteredBy: TempTarget.manual,
-            reason: TempTarget.cancel
+            enteredBy: TempTarget.local,
+            reason: TempTarget.cancel,
+            isPreset: nil,
+            enabled: nil,
+            halfBasalTarget: 160
         )
     }
 }
@@ -49,5 +55,8 @@ extension TempTarget {
         case duration
         case enteredBy
         case reason
+        case isPreset
+        case enabled
+        case halfBasalTarget
     }
 }

+ 2 - 2
FreeAPS/Sources/Modules/OverrideConfig/OverrideDataFlow.swift

@@ -1,7 +1,7 @@
 import Foundation
 import SwiftUI
 
-enum OverrideConfig {
+enum Adjustments {
     enum Config {}
 
     enum Tab: String, Hashable, Identifiable, CaseIterable {
@@ -24,4 +24,4 @@ enum OverrideConfig {
     }
 }
 
-protocol OverrideProvider: Provider {}
+protocol AdjustmentsProvider: Provider {}

+ 9 - 0
FreeAPS/Sources/Modules/Adjustments/AdjustmentsProvider.swift

@@ -0,0 +1,9 @@
+extension Adjustments {
+    final class Provider: BaseProvider, AdjustmentsProvider {
+        func getBGTarget() async -> BGTargets {
+            await storage.retrieveAsync(OpenAPS.Settings.bgTargets, as: BGTargets.self)
+                ?? BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
+                ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
+        }
+    }
+}

+ 93 - 0
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift

@@ -0,0 +1,93 @@
+import SwiftUI
+
+extension Adjustments.StateModel {
+    /// Returns a description of how insulin doses are adjusted based on percentage.
+    func percentageDescription(_ percent: Double) -> Text? {
+        if percent.isNaN || percent == 100 { return nil }
+
+        var description: String = "Insulin doses will be "
+
+        if percent < 100 {
+            description += "decreased by "
+        } else {
+            description += "increased by "
+        }
+
+        let deviationFrom100 = abs(percent - 100)
+        description += String(format: "%.0f% %.", deviationFrom100)
+
+        return Text(description)
+    }
+
+    /// Checks if the device is using a 24-hour time format.
+    func is24HourFormat() -> Bool {
+        let formatter = DateFormatter()
+        formatter.locale = Locale.current
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        let dateString = formatter.string(from: Date())
+
+        return !dateString.contains("AM") && !dateString.contains("PM")
+    }
+
+    /// Converts a given hour to a 12-hour AM/PM format string.
+    func convertTo12HourFormat(_ hour: Int) -> String {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "h a"
+
+        let calendar = Calendar.current
+        let components = DateComponents(hour: hour)
+        let date = calendar.date(from: components) ?? Date()
+
+        return formatter.string(from: date)
+    }
+
+    /// Formats a given 24-hour time number as a two-digit string.
+    func format24Hour(_ hour: Int) -> String {
+        String(format: "%02d", hour)
+    }
+
+    /// Converts a duration in minutes to a formatted string (e.g., "1 hr 30 min").
+    func formatHrMin(_ durationInMinutes: Int) -> String {
+        let hours = durationInMinutes / 60
+        let minutes = durationInMinutes % 60
+
+        switch (hours, minutes) {
+        case let (0, m):
+            return "\(m) min"
+        case let (h, 0):
+            return "\(h) hr"
+        default:
+            return "\(hours) hr \(minutes) min"
+        }
+    }
+
+    /// Converts hours and minutes to total minutes as a `Decimal`.
+    func convertToMinutes(_ hours: Int, _ minutes: Int) -> Decimal {
+        let totalMinutes = (hours * 60) + minutes
+        return Decimal(max(0, totalMinutes))
+    }
+}
+
+extension PickerSettingsProvider {
+    /// Generates picker values based on a setting, optionally rounding minimum to the nearest step.
+    func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits, roundMinToStep: Bool) -> [Decimal] {
+        if !roundMinToStep {
+            return generatePickerValues(from: setting, units: units)
+        }
+
+        // Adjust min to be divisible by step
+        var newSetting = setting
+        var min = Double(newSetting.min)
+        let step = Double(newSetting.step)
+        let remainder = min.truncatingRemainder(dividingBy: step)
+        if remainder != 0 {
+            // Move min up to the next value divisible by targetStep
+            min += (step - remainder)
+        }
+
+        newSetting.min = Decimal(min)
+
+        return generatePickerValues(from: newSetting, units: units)
+    }
+}

+ 338 - 0
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -0,0 +1,338 @@
+import Combine
+import CoreData
+import Foundation
+
+extension Adjustments.StateModel {
+    // MARK: - Enact Overrides
+
+    /// Enacts an Override Preset by enabling it and disabling others.
+    @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
+        do {
+            let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
+            overrideToEnact?.enabled = true
+            overrideToEnact?.date = Date()
+            overrideToEnact?.isUploadedToNS = false
+            isEnabled = true
+
+            await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
+            await resetStateVariables()
+
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+
+            updateLatestOverrideConfiguration()
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
+        }
+    }
+
+    // MARK: - Disable Overrides
+
+    /// Disables all active Overrides, optionally creating a run entry.
+    @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
+        // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
+
+        await viewContext.perform {
+            do {
+                // Fetch the existing OverrideStored objects from the context
+                let results = try ids.compactMap { id in
+                    try self.viewContext.existingObject(with: id) as? OverrideStored
+                }
+                guard !results.isEmpty else { return }
+
+                // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
+                if createOverrideRunEntry {
+                    // Use the first override to create a new OverrideRunStored entry
+                    if let canceledOverride = results.first {
+                        let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
+                        newOverrideRunStored.id = UUID()
+                        newOverrideRunStored.name = canceledOverride.name
+                        newOverrideRunStored.startDate = canceledOverride.date ?? .distantPast
+                        newOverrideRunStored.endDate = Date()
+                        newOverrideRunStored
+                            .target = NSDecimalNumber(decimal: self.overrideStorage.calculateTarget(override: canceledOverride))
+                        newOverrideRunStored.override = canceledOverride
+                        newOverrideRunStored.isUploadedToNS = false
+                    }
+                }
+
+                // Disable all overrides except the one with overrideID
+                for overrideToCancel in results where overrideToCancel.objectID != overrideID {
+                    overrideToCancel.enabled = false
+                }
+
+                if self.viewContext.hasChanges {
+                    // Save changes and update the View
+                    try self.viewContext.save()
+                    self.updateLatestOverrideConfiguration()
+                }
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides: \(error.localizedDescription)"
+                )
+            }
+        }
+    }
+
+    // MARK: - Save Overrides
+
+    /// Saves a custom Override and activates it.
+    func saveCustomOverride() async {
+        let override = Override(
+            name: overrideName,
+            enabled: true,
+            date: Date(),
+            duration: overrideDuration,
+            indefinite: indefinite,
+            percentage: overridePercentage,
+            smbIsOff: smbIsOff,
+            isPreset: isPreset,
+            id: id,
+            overrideTarget: shouldOverrideTarget,
+            target: target,
+            advancedSettings: advancedSettings,
+            isfAndCr: isfAndCr,
+            isf: isf,
+            cr: cr,
+            smbIsScheduledOff: smbIsScheduledOff,
+            start: start,
+            end: end,
+            smbMinutes: smbMinutes,
+            uamMinutes: uamMinutes
+        )
+
+        // First disable all Overrides
+        await disableAllActiveOverrides(createOverrideRunEntry: true)
+
+        // Then save and activate a new custom Override
+        await overrideStorage.storeOverride(override: override)
+
+        // Reset State variables
+        await resetStateVariables()
+
+        // Update View
+        updateLatestOverrideConfiguration()
+    }
+
+    /// Saves an Override Preset without activating it.
+    /// `enabled` has to be false
+    /// `isPreset` has to be true
+    func saveOverridePreset() async {
+        let preset = Override(
+            name: overrideName,
+            enabled: false,
+            date: Date(),
+            duration: overrideDuration,
+            indefinite: indefinite,
+            percentage: overridePercentage,
+            smbIsOff: smbIsOff,
+            isPreset: true,
+            id: id,
+            overrideTarget: shouldOverrideTarget,
+            target: target,
+            advancedSettings: advancedSettings,
+            isfAndCr: isfAndCr,
+            isf: isf,
+            cr: cr,
+            smbIsScheduledOff: smbIsScheduledOff,
+            start: start,
+            end: end,
+            smbMinutes: smbMinutes,
+            uamMinutes: uamMinutes
+        )
+
+        async let storeOverride: () = overrideStorage.storeOverride(override: preset)
+        async let resetState: () = resetStateVariables()
+        _ = await (storeOverride, resetState)
+        setupOverridePresetsArray()
+        await nightscoutManager.uploadProfiles()
+    }
+
+    // MARK: - Override Preset Management
+
+    /// Sets up the array of Override Presets for UI display.
+    func setupOverridePresetsArray() {
+        Task {
+            let ids = await overrideStorage.fetchForOverridePresets()
+            await updateOverridePresetsArray(with: ids)
+        }
+    }
+
+    /// Updates the array of Override Presets from Core Data.
+    @MainActor private func updateOverridePresetsArray(with IDs: [NSManagedObjectID]) async {
+        do {
+            let overrideObjects = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OverrideStored
+            }
+            overridePresets = overrideObjects
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    /// Deletes an Override Preset and updates the view.
+    func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
+        await overrideStorage.deleteOverridePreset(objectID)
+        setupOverridePresetsArray()
+        await nightscoutManager.uploadProfiles()
+    }
+
+    // MARK: - Update Latest Override Configuration
+
+    /// Updates the latest Override configuration and state.
+    /// First get the latest Overrides corresponding NSManagedObjectID with a background fetch
+    /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
+    /// This also needs to be called when we cancel an Override via the Home View to update the State of the Button for this case
+    func updateLatestOverrideConfiguration() {
+        Task {
+            let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
+            async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
+            async let setOverride: () = setCurrentOverride(from: id)
+            _ = await (updateState, setOverride)
+        }
+    }
+
+    /// Updates state variables with the latest Override configuration.
+    @MainActor func updateLatestOverrideConfigurationOfState(from IDs: [NSManagedObjectID]) async {
+        do {
+            let result = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? OverrideStored
+            }
+            isEnabled = result.first?.enabled ?? false
+            if !isEnabled {
+                await resetStateVariables()
+            }
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest Override configuration")
+        }
+    }
+
+    /// Sets the current active Override for UI purposes.
+    @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
+        do {
+            guard let firstID = IDs.first else {
+                activeOverrideName = "Custom Override"
+                currentActiveOverride = nil
+                return
+            }
+
+            if let overrideToEdit = try viewContext.existingObject(with: firstID) as? OverrideStored {
+                currentActiveOverride = overrideToEdit
+                activeOverrideName = overrideToEdit.name ?? "Custom Override"
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active Override: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    /// Duplicates the active Override Preset and cancels the previous one.
+    @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
+        guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset else { return }
+
+        let duplicateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
+
+        do {
+            try await viewContext.perform {
+                overridePresetToDuplicate.enabled = false
+                guard self.viewContext.hasChanges else { return }
+                try self.viewContext.save()
+            }
+
+            if let overrideToEdit = try viewContext.existingObject(with: duplicateId) as? OverrideStored {
+                currentActiveOverride = overrideToEdit
+                activeOverrideName = overrideToEdit.name ?? "Custom Override"
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous Override: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // MARK: - Helper Functions
+
+    /// Resets state variables to default values.
+    @MainActor func resetStateVariables() async {
+        id = ""
+        overrideDuration = 0
+        indefinite = true
+        overridePercentage = 100
+        advancedSettings = false
+        smbIsOff = false
+        overrideName = ""
+        shouldOverrideTarget = false
+        isf = true
+        cr = true
+        isfAndCr = true
+        smbIsScheduledOff = false
+        start = 0
+        end = 0
+        smbMinutes = defaultSmbMinutes
+        uamMinutes = defaultUamMinutes
+        target = currentGlucoseTarget
+    }
+
+    /// Rounds a target value to the nearest step.
+    static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
+        // Convert target and step to NSDecimalNumber
+        guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
+              let stepValue = NSDecimalNumber(decimal: step).doubleValue as Double?
+        else {
+            return target
+        }
+
+        // Perform the remainder check using truncatingRemainder
+        let remainder = Decimal(targetValue.truncatingRemainder(dividingBy: stepValue))
+
+        if remainder != 0 {
+            // Calculate how much to adjust (up or down) based on the remainder
+            let adjustment = step - remainder
+            return target + adjustment
+        }
+
+        // Return the original target if no adjustment is needed
+        return target
+    }
+
+    /// Rounds an Override percentage to the nearest step.
+    static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
+        let stepDouble = Double(step)
+        // Check if overridePercentage is not divisible by the selected step
+        if percentage.truncatingRemainder(dividingBy: stepDouble) != 0 {
+            let roundedValue: Double
+
+            if percentage > 100 {
+                // Round down to the nearest valid step away from 100
+                let stepCount = (percentage - 100) / stepDouble
+                roundedValue = 100 + floor(stepCount) * stepDouble
+            } else {
+                // Round up to the nearest valid step away from 100
+                let stepCount = (100 - percentage) / stepDouble
+                roundedValue = 100 - floor(stepCount) * stepDouble
+            }
+
+            // Ensure the value stays between 10 and 200
+            return max(10, min(roundedValue, 200))
+        }
+
+        return percentage
+    }
+}
+
+enum IsfAndOrCrOptions: String, CaseIterable {
+    case isfAndCr = "ISF/CR"
+    case isf = "ISF"
+    case cr = "CR"
+    case nothing = "None"
+}
+
+enum DisableSmbOptions: String, CaseIterable {
+    case dontDisable = "Don't Disable"
+    case disable = "Disable"
+    case disableOnSchedule = "Disable on Schedule"
+}

+ 422 - 0
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -0,0 +1,422 @@
+import Combine
+import CoreData
+import Foundation
+
+extension Adjustments.StateModel {
+    // MARK: - State Initialization and Updates
+
+    /// Updates the latest Temp Target configuration for UI state and logic.
+    /// First get the latest Temp Target corresponding NSManagedObjectID with a background fetch
+    /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
+    /// This also needs to be called when we cancel an Temp Target via the Home View to update the State of the Button for this case
+    func updateLatestTempTargetConfiguration() {
+        Task {
+            let id = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
+            async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
+            async let setTempTarget: () = setCurrentTempTarget(from: id)
+            _ = await (updateState, setTempTarget)
+        }
+    }
+
+    /// Updates state variables with the latest Temp Target configuration.
+    @MainActor func updateLatestTempTargetConfigurationOfState(from IDs: [NSManagedObjectID]) async {
+        do {
+            let result = try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? TempTargetStored
+            }
+            isTempTargetEnabled = result.first?.enabled ?? false
+            if !isEnabled {
+                await resetTempTargetState()
+            }
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest temp target configuration")
+        }
+    }
+
+    /// Sets the current Temp Target for UI and logic purposes.
+    @MainActor func setCurrentTempTarget(from IDs: [NSManagedObjectID]) async {
+        do {
+            guard let firstID = IDs.first else {
+                activeTempTargetName = "Custom Temp Target"
+                currentActiveTempTarget = nil
+                return
+            }
+
+            if let tempTargetToEdit = try viewContext.existingObject(with: firstID) as? TempTargetStored {
+                currentActiveTempTarget = tempTargetToEdit
+                activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
+                tempTargetTarget = tempTargetToEdit.target?.decimalValue ?? 0
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    // MARK: - Temp Target Fetching and Setup
+
+    /// Sets up Temp Targets using fetch and update functions.
+    func setupTempTargets(
+        fetchFunction: @escaping () async -> [NSManagedObjectID],
+        updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
+    ) {
+        Task {
+            let ids = await fetchFunction()
+            let tempTargetObjects = await fetchTempTargetObjects(for: ids)
+            await updateFunction(tempTargetObjects)
+        }
+    }
+
+    /// Fetches Temp Target objects from Core Data.
+    @MainActor private func fetchTempTargetObjects(for IDs: [NSManagedObjectID]) async -> [TempTargetStored] {
+        do {
+            return try IDs.compactMap { id in
+                try viewContext.existingObject(with: id) as? TempTargetStored
+            }
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Targets")
+            return []
+        }
+    }
+
+    /// Sets up the Temp Target presets array for the view.
+    func setupTempTargetPresetsArray() {
+        setupTempTargets(
+            fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
+            updateFunction: { tempTargets in
+                self.tempTargetPresets = tempTargets
+            }
+        )
+    }
+
+    /// Sets up the scheduled Temp Targets array for the view.
+    func setupScheduledTempTargetsArray() {
+        setupTempTargets(
+            fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
+            updateFunction: { tempTargets in
+                self.scheduledTempTargets = tempTargets
+            }
+        )
+    }
+
+    // MARK: - Temp Target Creation and Management
+
+    /// Saves a Temp Target to storage.
+    func saveTempTargetToStorage(tempTargets: [TempTarget]) {
+        tempTargetStorage.saveTempTargetsToStorage(tempTargets)
+    }
+
+    /// Saves a Temp Target based on whether it is scheduled or custom.
+    func invokeSaveOfCustomTempTargets() async {
+        if date > Date() {
+            await saveScheduledTempTarget()
+        } else {
+            await saveCustomTempTarget()
+        }
+    }
+
+    /// Saves a scheduled Temp Target and activates it at the specified date.
+    func saveScheduledTempTarget() async {
+        let date = self.date
+        guard date > Date() else { return }
+
+        let tempTarget = TempTarget(
+            name: tempTargetName,
+            createdAt: date,
+            targetTop: tempTargetTarget,
+            targetBottom: tempTargetTarget,
+            duration: tempTargetDuration,
+            enteredBy: TempTarget.local,
+            reason: TempTarget.custom,
+            isPreset: false,
+            enabled: false,
+            halfBasalTarget: halfBasalTarget
+        )
+        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        setupScheduledTempTargetsArray()
+
+        Task {
+            await waitUntilDate(date)
+            await disableAllActiveTempTargets(createTempTargetRunEntry: true)
+            await enableScheduledTempTarget(for: date)
+            tempTargetStorage.saveTempTargetsToStorage([tempTarget])
+        }
+    }
+
+    /// Enables a scheduled Temp Target for a specific date.
+    func enableScheduledTempTarget(for date: Date) async {
+        let ids = await tempTargetStorage.fetchScheduledTempTarget(for: date)
+        guard let firstID = ids.first else {
+            debugPrint("No Temp Target found for the specified date.")
+            return
+        }
+        await setCurrentTempTarget(from: ids)
+
+        await MainActor.run {
+            do {
+                if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
+                    tempTarget.enabled = true
+                    try viewContext.save()
+                    isTempTargetEnabled = true
+                }
+            } catch {
+                debugPrint("Failed to enable the Temp Target: \(error.localizedDescription)")
+            }
+        }
+        setupScheduledTempTargetsArray()
+    }
+
+    /// Waits until a target date before proceeding.
+    private func waitUntilDate(_ targetDate: Date) async {
+        while Date() < targetDate {
+            let timeInterval = targetDate.timeIntervalSince(Date())
+            let sleepDuration = min(timeInterval, 60.0)
+            try? await Task.sleep(nanoseconds: UInt64(sleepDuration * 1_000_000_000))
+        }
+    }
+
+    /// Saves a custom Temp Target and disables existing ones.
+    func saveCustomTempTarget() async {
+        await disableAllActiveTempTargets(createTempTargetRunEntry: true)
+        let tempTarget = TempTarget(
+            name: tempTargetName,
+            /// We don't need to use the state var date here as we are using a different function for scheduled Temp Targets 'saveScheduledTempTarget()'
+            createdAt: Date(),
+            targetTop: tempTargetTarget,
+            targetBottom: tempTargetTarget,
+            duration: tempTargetDuration,
+            enteredBy: TempTarget.local,
+            reason: TempTarget.custom,
+            isPreset: false,
+            enabled: true,
+            halfBasalTarget: halfBasalTarget
+        )
+        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        tempTargetStorage.saveTempTargetsToStorage([tempTarget])
+        await resetTempTargetState()
+        isTempTargetEnabled = true
+        updateLatestTempTargetConfiguration()
+    }
+
+    /// Creates a new Temp Target preset.
+    func saveTempTargetPreset() async {
+        let tempTarget = TempTarget(
+            name: tempTargetName,
+            createdAt: Date(),
+            targetTop: tempTargetTarget,
+            targetBottom: tempTargetTarget,
+            duration: tempTargetDuration,
+            enteredBy: TempTarget.local,
+            reason: TempTarget.custom,
+            isPreset: true,
+            enabled: false,
+            halfBasalTarget: halfBasalTarget
+        )
+        await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
+        await resetTempTargetState()
+        setupTempTargetPresetsArray()
+    }
+
+    /// Enacts a Temp Target preset by enabling it.
+    @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
+        do {
+            let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
+            tempTargetToEnact?.enabled = true
+            tempTargetToEnact?.date = Date()
+            tempTargetToEnact?.isUploadedToNS = false
+            isTempTargetEnabled = true
+
+            async let disableTempTargets: () = disableAllActiveTempTargets(
+                except: id,
+                createTempTargetRunEntry: currentActiveTempTarget != nil
+            )
+            async let resetState: () = resetTempTargetState()
+            _ = await (disableTempTargets, resetState)
+
+            if viewContext.hasChanges {
+                try viewContext.save()
+            }
+
+            updateLatestTempTargetConfiguration()
+
+            let tempTarget = TempTarget(
+                name: tempTargetToEnact?.name,
+                createdAt: Date(),
+                targetTop: tempTargetToEnact?.target?.decimalValue,
+                targetBottom: tempTargetToEnact?.target?.decimalValue,
+                duration: tempTargetToEnact?.duration?.decimalValue ?? 0,
+                enteredBy: TempTarget.local,
+                reason: TempTarget.custom,
+                isPreset: true,
+                enabled: true,
+                halfBasalTarget: halfBasalTarget
+            )
+            tempTargetStorage.saveTempTargetsToStorage([tempTarget])
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
+        }
+    }
+
+    /// Disables all active Temp Targets.
+    @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
+        // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
+        let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
+
+        await viewContext.perform {
+            do {
+                // Fetch the existing TempTargetStored objects from the context
+                let results = try ids.compactMap { id in
+                    try self.viewContext.existingObject(with: id) as? TempTargetStored
+                }
+
+                // If there are no results, return early
+                guard !results.isEmpty else { return }
+
+                // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
+                if createTempTargetRunEntry {
+                    // Use the first temp target to create a new TempTargetRunStored entry
+                    if let canceledTempTarget = results.first {
+                        let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
+                        newTempTargetRunStored.id = UUID()
+                        newTempTargetRunStored.name = canceledTempTarget.name
+                        newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
+                        newTempTargetRunStored.endDate = Date()
+                        newTempTargetRunStored
+                            .target = canceledTempTarget.target ?? 0
+                        newTempTargetRunStored.tempTarget = canceledTempTarget
+                        newTempTargetRunStored.isUploadedToNS = false
+                    }
+                }
+
+                // Disable all temporary targets except the one with given id
+                for tempTargetToCancel in results {
+                    if tempTargetToCancel.objectID != id {
+                        tempTargetToCancel.enabled = false
+                    }
+                }
+
+                // Save the context if there are changes
+                if self.viewContext.hasChanges {
+                    try self.viewContext.save()
+
+                    // Update the storage
+                    self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
+                }
+            } catch {
+                debugPrint(
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
+                )
+            }
+        }
+    }
+
+    /// Duplicates the current preset and cancels the previous one.
+    @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
+        // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
+        guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
+              tempTargetPresetToDuplicate.isPreset == true else { return }
+
+        // Copy the current TempTarget-Preset to not edit the underlying Preset
+        let duplidateId = await tempTargetStorage.copyRunningTempTarget(tempTargetPresetToDuplicate)
+
+        // Cancel the duplicated Temp Target
+        // As we are on the Main Thread already we don't need to cancel via the objectID in this case
+        do {
+            try await viewContext.perform {
+                tempTargetPresetToDuplicate.enabled = false
+
+                guard self.viewContext.hasChanges else { return }
+                try self.viewContext.save()
+            }
+
+            if let tempTargetToEdit = try viewContext.existingObject(with: duplidateId) as? TempTargetStored
+            {
+                currentActiveTempTarget = tempTargetToEdit
+                activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
+            }
+        } catch {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
+            )
+        }
+    }
+
+    /// Deletes a Temp Target preset.
+    func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
+        await tempTargetStorage.deleteOverridePreset(objectID)
+        setupTempTargetPresetsArray()
+    }
+
+    /// Resets Temp Target state variables.
+    @MainActor func resetTempTargetState() async {
+        tempTargetName = ""
+        tempTargetTarget = 100
+        tempTargetDuration = 0
+        percentage = 100
+        halfBasalTarget = settingHalfBasalTarget
+    }
+
+    // MARK: - Calculations
+
+    /// Computes the half-basal target based on the current settings.
+    func computeHalfBasalTarget(
+        usingTarget initialTarget: Decimal? = nil,
+        usingPercentage initialPercentage: Double? = nil
+    ) -> Double {
+        let adjustmentPercentage = initialPercentage ?? percentage
+        let adjustmentRatio = Decimal(adjustmentPercentage / 100)
+        let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
+        var halfBasalTargetValue = halfBasalTarget
+        if adjustmentRatio != 1 {
+            halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
+                (adjustmentRatio - 1)
+        }
+        return round(Double(halfBasalTargetValue))
+    }
+
+    /// Determines if sensitivity adjustment is enabled based on target.
+    func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
+        let target = initialTarget ?? tempTargetTarget
+        if target < normalTarget, lowTTlowersSens { return true }
+        if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
+        return false
+    }
+
+    /// Computes the low value for the slider based on the target.
+    func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
+        let calcTarget = initialTarget ?? tempTargetTarget
+        guard calcTarget != 0 else { return 15 } // oref defined maximum sensitivity
+        let minSens = calcTarget < normalTarget ? 105 : 15
+        return Double(max(0, minSens))
+    }
+
+    /// Computes the high value for the slider based on the target.
+    func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
+        let calcTarget = initialTarget ?? tempTargetTarget
+        guard calcTarget != 0 else { return Double(maxValue * 100) } // oref defined limit for increased insulin delivery
+        let maxSens = calcTarget > normalTarget ? 95 : Double(maxValue * 100)
+        return maxSens
+    }
+
+    /// Computes the adjusted percentage for the slider.
+    func computeAdjustedPercentage(
+        usingHBT initialHalfBasalTarget: Decimal? = nil,
+        usingTarget initialTarget: Decimal? = nil
+    ) -> Double {
+        let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
+        let calcTarget = initialTarget ?? tempTargetTarget
+        let deviationFromNormal = halfBasalTargetValue - normalTarget
+
+        let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
+            adjustmentFactor
+
+        return Double(min(adjustmentRatio, maxValue) * 100).rounded()
+    }
+}
+
+enum TempTargetSensitivityAdjustmentType: String, CaseIterable {
+    case standard = "Standard"
+    case slider = "Custom"
+}

+ 272 - 0
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -0,0 +1,272 @@
+import Combine
+import CoreData
+import Observation
+import SwiftUI
+
+extension Adjustments {
+    @Observable final class StateModel: BaseStateModel<Provider> {
+        // MARK: - Injected Dependencies
+
+        @ObservationIgnored @Injected() var broadcaster: Broadcaster!
+        @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
+        @ObservationIgnored @Injected() var apsManager: APSManager!
+        @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
+        @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
+
+        // MARK: - Override and Temp Target Properties
+
+        var overridePercentage: Double = 100
+        var isEnabled = false
+        var indefinite = true
+        var overrideDuration: Decimal = 0
+        var target: Decimal = 0
+        var currentGlucoseTarget: Decimal = 100
+        var shouldOverrideTarget: Bool = false
+        var smbIsOff: Bool = false
+        var id = ""
+        var overrideName: String = ""
+        var isPreset: Bool = false
+        var overridePresets: [OverrideStored] = []
+        var advancedSettings: Bool = false
+        var isfAndCr: Bool = true
+        var isf: Bool = true
+        var cr: Bool = true
+        var smbIsScheduledOff: Bool = false
+        var start: Decimal = 0
+        var end: Decimal = 0
+        var smbMinutes: Decimal = 0
+        var uamMinutes: Decimal = 0
+        var defaultSmbMinutes: Decimal = 0
+        var defaultUamMinutes: Decimal = 0
+        var selectedTab: Tab = .overrides
+        var activeOverrideName: String = ""
+        var currentActiveOverride: OverrideStored?
+        var activeTempTargetName: String = ""
+
+        var currentActiveTempTarget: TempTargetStored?
+        var showOverrideEditSheet = false
+        var showTempTargetEditSheet = false
+        var units: GlucoseUnits = .mgdL
+
+        // Temp Target Properties
+        let normalTarget: Decimal = 100
+        var tempTargetDuration: Decimal = 0
+        var tempTargetName: String = ""
+        var tempTargetTarget: Decimal = 100
+        var isTempTargetEnabled: Bool = false
+        var date = Date()
+        var newPresetName = ""
+        var tempTargetPresets: [TempTargetStored] = []
+        var scheduledTempTargets: [TempTargetStored] = []
+        var percentage: Double = 100
+        var maxValue: Decimal = 1.2
+        var halfBasalTarget: Decimal = 160
+        var settingHalfBasalTarget: Decimal = 160
+        var highTTraisesSens: Bool = false
+        var isExerciseModeActive: Bool = false
+        var lowTTlowersSens: Bool = false
+        var didSaveSettings: Bool = false
+
+        // Core Data
+        let coredataContext = CoreDataStack.shared.newTaskContext()
+        let viewContext = CoreDataStack.shared.persistentContainer.viewContext
+
+        // Help Sheet
+        var isHelpSheetPresented: Bool = false
+        var helpSheetDetent = PresentationDetent.large
+
+        // Combine
+        private var cancellables = Set<AnyCancellable>()
+
+        // MARK: - Lifecycle
+
+        /// Subscribes to notifications and initializes settings.
+        override func subscribe() {
+            setupNotification()
+            setupSettings()
+            broadcaster.register(SettingsObserver.self, observer: self)
+            broadcaster.register(PreferencesObserver.self, observer: self)
+
+            Task {
+                await withTaskGroup(of: Void.self) { group in
+                    group.addTask { self.setupOverridePresetsArray() }
+                    group.addTask { self.setupTempTargetPresetsArray() }
+                    group.addTask { self.updateLatestOverrideConfiguration() }
+                    group.addTask { self.updateLatestTempTargetConfiguration() }
+                }
+            }
+        }
+
+        /// Retrieves the current glucose target based on the time of day.
+        func getCurrentGlucoseTarget() async {
+            let now = Date()
+            let calendar = Calendar.current
+            let dateFormatter = DateFormatter()
+            dateFormatter.dateFormat = "HH:mm:ss"
+            dateFormatter.timeZone = TimeZone.current
+
+            let bgTargets = await provider.getBGTarget()
+            let entries: [(start: String, value: Decimal)] = bgTargets.targets.map { ($0.start, $0.low) }
+
+            for (index, entry) in entries.enumerated() {
+                guard let entryTime = dateFormatter.date(from: entry.start) else {
+                    print("Invalid entry start time: \(entry.start)")
+                    continue
+                }
+
+                let entryComponents = calendar.dateComponents([.hour, .minute, .second], from: entryTime)
+                let entryStartTime = calendar.date(
+                    bySettingHour: entryComponents.hour!,
+                    minute: entryComponents.minute!,
+                    second: entryComponents.second!,
+                    of: now
+                )!
+
+                let entryEndTime: Date
+                if index < entries.count - 1,
+                   let nextEntryTime = dateFormatter.date(from: entries[index + 1].start)
+                {
+                    let nextEntryComponents = calendar.dateComponents([.hour, .minute, .second], from: nextEntryTime)
+                    entryEndTime = calendar.date(
+                        bySettingHour: nextEntryComponents.hour!,
+                        minute: nextEntryComponents.minute!,
+                        second: nextEntryComponents.second!,
+                        of: now
+                    )!
+                } else {
+                    entryEndTime = calendar.date(byAdding: .day, value: 1, to: entryStartTime)!
+                }
+
+                if now >= entryStartTime, now < entryEndTime {
+                    await MainActor.run {
+                        currentGlucoseTarget = entry.value
+                        target = currentGlucoseTarget
+                    }
+                    return
+                }
+            }
+        }
+
+        /// Configures various settings from the settings manager.
+        private func setupSettings() {
+            units = settingsManager.settings.units
+            defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
+            defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
+            maxValue = settingsManager.preferences.autosensMax
+            settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+            halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+            highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
+            isExerciseModeActive = settingsManager.preferences.exerciseMode
+            lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
+            percentage = computeAdjustedPercentage()
+            Task {
+                await getCurrentGlucoseTarget()
+            }
+        }
+
+        /// Reorders Override Presets and updates the view.
+        func reorderOverride(from source: IndexSet, to destination: Int) {
+            overridePresets.move(fromOffsets: source, toOffset: destination)
+            for (index, override) in overridePresets.enumerated() {
+                override.orderPosition = Int16(index + 1)
+            }
+            do {
+                guard viewContext.hasChanges else { return }
+                try viewContext.save()
+                setupOverridePresetsArray()
+                Task { await nightscoutManager.uploadProfiles() }
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Presets order")
+            }
+        }
+
+        /// Reorders Temp Target Presets and updates the view.
+        func reorderTempTargets(from source: IndexSet, to destination: Int) {
+            tempTargetPresets.move(fromOffsets: source, toOffset: destination)
+            for (index, tempTarget) in tempTargetPresets.enumerated() {
+                tempTarget.orderPosition = Int16(index + 1)
+            }
+            do {
+                guard viewContext.hasChanges else { return }
+                try viewContext.save()
+                setupTempTargetPresetsArray()
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target Presets order")
+            }
+        }
+    }
+}
+
+// MARK: - Notifications Setup
+
+extension Adjustments.StateModel {
+    /// Sets up notification observers for Override and Temp Target updates.
+    func setupNotification() {
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleOverrideConfigurationUpdate),
+            name: .didUpdateOverrideConfiguration,
+            object: nil
+        )
+
+        // Custom Notification to update View when an Temp Target has been cancelled via Home View
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(handleTempTargetConfigurationUpdate),
+            name: .didUpdateTempTargetConfiguration,
+            object: nil
+        )
+
+        // Creates a publisher that updates the Override View when the Custom notification was sent (via shortcut)
+        Foundation.NotificationCenter.default.publisher(for: .willUpdateOverrideConfiguration)
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.updateLatestOverrideConfiguration()
+            }
+            .store(in: &cancellables)
+
+        // Creates a publisher that updates the Temp Target View when the Custom notification was sent (via shortcut)
+        Foundation.NotificationCenter.default.publisher(for: .willUpdateTempTargetConfiguration)
+            .sink { [weak self] _ in
+                guard let self = self else { return }
+                self.updateLatestTempTargetConfiguration()
+            }
+            .store(in: &cancellables)
+    }
+
+    /// Handles Override configuration updates.
+    @objc private func handleOverrideConfigurationUpdate() {
+        updateLatestOverrideConfiguration()
+    }
+
+    /// Handles Temp Target configuration updates.
+    @objc private func handleTempTargetConfigurationUpdate() {
+        updateLatestTempTargetConfiguration()
+    }
+}
+
+extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
+    /// Updates settings when they change.
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+        Task {
+            await getCurrentGlucoseTarget()
+        }
+    }
+
+    /// Updates preferences when they change.
+    func preferencesDidChange(_: Preferences) {
+        defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
+        defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
+        maxValue = settingsManager.preferences.autosensMax
+        settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+        halfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+        highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
+        isExerciseModeActive = settingsManager.preferences.exerciseMode
+        lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
+        percentage = computeAdjustedPercentage()
+        Task {
+            await getCurrentGlucoseTarget()
+        }
+    }
+}

+ 707 - 0
FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -0,0 +1,707 @@
+import CoreData
+import SwiftUI
+import Swinject
+
+extension Adjustments {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @State var state = StateModel()
+        @State private var isEditing = false
+        @State private var showOverrideCreationSheet = false
+        @State private var showTempTargetCreationSheet = false
+        @State private var showingDetail = false
+        @State private var showCheckmark: Bool = false
+        @State private var selectedPresetID: String?
+        @State private var selectedTempTargetPresetID: String?
+        @State private var selectedOverride: OverrideStored?
+        @State private var selectedTempTarget: TempTargetStored?
+        @State private var isConfirmDeletePresented = false
+        @State private var isPromptPresented = false
+        @State private var isRemoveAlertPresented = false
+        @State private var removeAlert: Alert?
+        @State private var isEditingTT = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        private func formattedGlucose(glucose: Decimal) -> String {
+            let formattedValue: String
+            if state.units == .mgdL {
+                formattedValue = Formatter.glucoseFormatter(for: state.units)
+                    .string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
+            } else {
+                formattedValue = glucose.formattedAsMmolL
+            }
+            return "\(formattedValue) \(state.units.rawValue)"
+        }
+
+        var body: some View {
+            ZStack(alignment: .center, content: {
+                VStack {
+                    Picker("Adjustment Tabs", selection: $state.selectedTab) {
+                        ForEach(Adjustments.Tab.allCases.indexed(), id: \.1) { index, item in
+                            Text(item.name).tag(index)
+                        }
+                    }
+                    .pickerStyle(SegmentedPickerStyle())
+                    .padding(.horizontal)
+
+                    List {
+                        switch state.selectedTab {
+                        case .overrides: overrides()
+                        case .tempTargets: tempTargets() }
+                    }
+                    .scrollContentBackground(.hidden)
+                    .background(appState.trioBackgroundColor(for: colorScheme))
+                }
+                .listSectionSpacing(10)
+                .safeAreaInset(edge: .bottom, spacing: 30) { stickyStopButton }
+                .scrollContentBackground(.hidden)
+                .background(appState.trioBackgroundColor(for: colorScheme))
+                .onAppear(perform: configureView)
+                .navigationBarTitle("Adjustments")
+                .navigationBarTitleDisplayMode(.large)
+                .toolbar {
+                    ToolbarItem(placement: .topBarTrailing) {
+                        switch state.selectedTab {
+                        case .overrides:
+                            Button(action: {
+                                showOverrideCreationSheet = true
+                            }, label: {
+                                HStack {
+                                    Text("Add Override")
+                                    Image(systemName: "plus")
+                                }
+                            })
+                        case .tempTargets:
+                            Button(action: {
+                                showTempTargetCreationSheet = true
+                            }, label: {
+                                HStack {
+                                    Text("Add Temp Target")
+                                    Image(systemName: "plus")
+                                }
+                            })
+                        }
+                    }
+                }
+                .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
+                    Task {
+                        await state.resetStateVariables()
+                        state.showOverrideEditSheet = false
+                    }
+
+                }) {
+                    if let override = selectedOverride {
+                        EditOverrideForm(overrideToEdit: override, state: state)
+                    }
+                }
+                .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
+                    Task {
+                        await state.resetStateVariables()
+                        showOverrideCreationSheet = false
+                    }
+                }) {
+                    AddOverrideForm(state: state)
+                }
+                .sheet(isPresented: $showTempTargetCreationSheet, onDismiss: {
+                    Task {
+                        await state.resetTempTargetState()
+                        showTempTargetCreationSheet = false
+                    }
+                }) {
+                    AddTempTargetForm(state: state)
+                }
+                .sheet(isPresented: $state.showTempTargetEditSheet, onDismiss: {
+                    Task {
+                        await state.resetTempTargetState()
+                        state.showTempTargetEditSheet = false
+                    }
+
+                }) {
+                    if let tempTarget = selectedTempTarget {
+                        EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
+                    }
+                }
+            }).background(appState.trioBackgroundColor(for: colorScheme))
+        }
+
+        @ViewBuilder func overrides() -> some View {
+            if state.isEnabled, state.activeOverrideName.isNotEmpty {
+                currentActiveAdjustment
+            }
+            if state.overridePresets.isNotEmpty {
+                overridePresets
+            } else {
+                defaultText
+            }
+        }
+
+        @ViewBuilder func tempTargets() -> some View {
+            if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
+                currentActiveAdjustment
+            }
+            if state.scheduledTempTargets.isNotEmpty {
+                scheduledTempTargets
+            }
+            if state.tempTargetPresets.isNotEmpty {
+                tempTargetPresets
+            } else {
+                defaultText
+            }
+        }
+
+        private var defaultText: some View {
+            switch state.selectedTab {
+            case .overrides:
+                Section {} header: {
+                    Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
+                        .textCase(nil)
+                        .foregroundStyle(.secondary)
+                }
+            case .tempTargets:
+                Section {} header: {
+                    Text(
+                        "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
+                    )
+                    .textCase(nil)
+                    .foregroundStyle(.secondary)
+                }
+            }
+        }
+
+        private var overridePresets: some View {
+            Section {
+                ForEach(state.overridePresets) { preset in
+                    overridesView(for: preset)
+                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                            Button(role: .none) {
+                                selectedOverride = preset
+                                isConfirmDeletePresented = true
+                            } label: {
+                                Label("Delete", systemImage: "trash")
+                                    .tint(.red)
+                            }
+                            Button(action: {
+                                // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
+                                selectedOverride = preset
+                                state.showOverrideEditSheet = true
+                            }, label: {
+                                Label("Edit", systemImage: "pencil")
+                                    .tint(.blue)
+                            })
+                        }
+                }
+                .onMove(perform: state.reorderOverride)
+                .confirmationDialog(
+                    "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
+                    isPresented: $isConfirmDeletePresented,
+                    titleVisibility: .visible
+                ) {
+                    if let itemToDelete = selectedOverride {
+                        Button(
+                            state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
+                            role: .destructive
+                        ) {
+                            if state.currentActiveOverride == selectedOverride {
+                                Task {
+                                    // Save cancelled Override in OverrideRunStored Entity
+                                    // Cancel ALL active Override
+                                    await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                                }
+                            }
+                            // Perform the delete action
+                            Task {
+                                await state.invokeOverridePresetDeletion(itemToDelete.objectID)
+                            }
+                            // Reset the selected item after deletion
+                            selectedOverride = nil
+                        }
+                    }
+                    Button("Cancel", role: .cancel) {
+                        // Dismiss the dialog without action
+                        selectedOverride = nil
+                    }
+                } message: {
+                    if state.currentActiveOverride == selectedOverride {
+                        Text(
+                            state
+                                .currentActiveOverride == selectedOverride ?
+                                "This override preset is currently running. Deleting will stop it." : ""
+                        )
+                    }
+                }
+                .listRowBackground(Color.chart)
+            } header: {
+                Text("Override Presets")
+            } footer: {
+                HStack {
+                    Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
+                    Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
+                }
+            }
+        }
+
+        private var scheduledTempTargets: some View {
+            Section {
+                ForEach(state.scheduledTempTargets) { tempTarget in
+                    tempTargetView(for: tempTarget)
+                        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                            swipeActions(for: tempTarget)
+                        }
+                }
+                .listRowBackground(Color.chart)
+            } header: {
+                Text("Scheduled Temp Targets")
+            }
+        }
+
+        private var tempTargetPresets: some View {
+            Section {
+                ForEach(state.tempTargetPresets) { preset in
+                    tempTargetView(for: preset, showCheckmark: showCheckmark) {
+                        enactTempTargetPreset(preset)
+                    }
+                    .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                        swipeActions(for: preset)
+                    }
+                }
+                .onMove(perform: state.reorderTempTargets)
+                .confirmationDialog(
+                    deleteConfirmationTitle,
+                    isPresented: $isConfirmDeletePresented,
+                    titleVisibility: .visible
+                ) {
+                    deleteConfirmationButtons()
+                } message: {
+                    deleteConfirmationMessage
+                }
+                .listRowBackground(Color.chart)
+            } header: {
+                Text("Temporary Target Presets")
+            } footer: {
+                HStack {
+                    Image(systemName: "hand.draw.fill").foregroundStyle(.primary)
+                    Text("Swipe left to edit or delete a temporary target preset. Hold, drag and drop to reorder a preset.")
+                }
+            }
+        }
+
+        private func enactTempTargetPreset(_ preset: TempTargetStored) {
+            Task {
+                let objectID = preset.objectID
+                await state.enactTempTargetPreset(withID: objectID)
+                selectedTempTargetPresetID = preset.id?.uuidString
+                showCheckmark.toggle()
+
+                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                    showCheckmark = false
+                }
+            }
+        }
+
+        private func swipeActions(for tempTarget: TempTargetStored) -> some View {
+            Group {
+                Button {
+                    Task {
+                        selectedTempTarget = tempTarget
+                        isConfirmDeletePresented = true
+                    }
+                } label: {
+                    Label("Delete", systemImage: "trash")
+                        .tint(.red)
+                }
+                Button(action: {
+                    selectedTempTarget = tempTarget
+                    state.showTempTargetEditSheet = true
+                }, label: {
+                    Label("Edit", systemImage: "pencil")
+                        .tint(.blue)
+                })
+            }
+        }
+
+        private var deleteConfirmationTitle: String {
+            "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?"
+        }
+
+        private func deleteConfirmationButtons() -> some View {
+            Group {
+                if let itemToDelete = selectedTempTarget {
+                    Button(
+                        state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
+                        role: .destructive
+                    ) {
+                        if state.currentActiveTempTarget == selectedTempTarget {
+                            Task {
+                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                            }
+                        }
+                        Task {
+                            await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
+                        }
+                        selectedTempTarget = nil
+                    }
+                }
+                Button("Cancel", role: .cancel) {
+                    selectedTempTarget = nil
+                }
+            }
+        }
+
+        private var deleteConfirmationMessage: Text? {
+            if state.currentActiveTempTarget == selectedTempTarget {
+                return Text("This Temp Target preset is currently running. Deleting will stop it.")
+            }
+            return nil
+        }
+
+        private var currentActiveAdjustment: some View {
+            switch state.selectedTab {
+            case .overrides:
+                Section {
+                    HStack {
+                        Text("\(state.activeOverrideName) is running")
+
+                        Spacer()
+                        Image(systemName: "square.and.pencil")
+                            .foregroundStyle(Color.primary)
+                    }
+                    .contentShape(Rectangle())
+                    .onTapGesture {
+                        Task {
+                            /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
+                            /// The currentActiveOverride variable in the State will update automatically via MOC notification
+                            await state.duplicateOverridePresetAndCancelPreviousOverride()
+
+                            /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
+                            selectedOverride = state.currentActiveOverride
+
+                            /// Now we can show the Edit sheet
+                            state.showOverrideEditSheet = true
+                        }
+                    }
+                }
+                .listRowBackground(Color.purple.opacity(0.8))
+            case .tempTargets:
+                Section {
+                    HStack {
+                        Text("\(state.activeTempTargetName) is running")
+
+                        Spacer()
+                        Image(systemName: "square.and.pencil")
+                            .foregroundStyle(Color.primary)
+                    }
+                    .contentShape(Rectangle())
+                    .onTapGesture {
+                        Task {
+                            /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
+                            /// The currentActiveOverride variable in the State will update automatically via MOC notification
+                            await state.duplicateTempTargetPresetAndCancelPreviousTempTarget()
+
+                            /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
+                            selectedTempTarget = state.currentActiveTempTarget
+
+                            /// Now we can show the Edit sheet
+                            state.showTempTargetEditSheet = true
+                        }
+                    }
+                }
+                .listRowBackground(Color.loopGreen.opacity(0.8))
+            }
+        }
+
+        var stickyStopButton: some View {
+            ZStack {
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 65)
+                    .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                    .background(.thinMaterial)
+                    .opacity(0.8)
+                    .clipShape(Rectangle())
+                Group {
+                    switch state.selectedTab {
+                    case .overrides:
+                        Button(action: {
+                            Task {
+                                // Save cancelled Override in OverrideRunStored Entity
+                                // Cancel ALL active Override
+                                await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                            }
+                        }, label: {
+                            Text("Stop Override")
+                                .padding(10)
+                        })
+                            .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                            .disabled(!state.isEnabled)
+                            .background(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
+                            .tint(.white)
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
+                    case .tempTargets:
+                        Button(action: {
+                            Task {
+                                // Save cancelled Temp Targets in TempTargetRunStored Entity
+                                // Cancel ALL active Temp Targets
+                                await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+                                // Update View
+                                state.updateLatestTempTargetConfiguration()
+                            }
+                        }, label: {
+                            Text("Stop Temp Target")
+                                .padding(10)
+                        })
+                            .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                            .disabled(!state.isTempTargetEnabled)
+                            .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
+                            .tint(.white)
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
+                    }
+                }.padding(5)
+            }
+        }
+
+        private var cancelAdjustmentButton: some View {
+            switch state.selectedTab {
+            case .overrides:
+                Button(action: {
+                    Task {
+                        // Save cancelled Override in OverrideRunStored Entity
+                        // Cancel ALL active Override
+                        await state.disableAllActiveOverrides(createOverrideRunEntry: true)
+                    }
+                }, label: {
+                    Text("Stop Override")
+
+                })
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .disabled(!state.isEnabled)
+                    .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
+                    .tint(.white)
+            case .tempTargets:
+                Button(action: {
+                    Task {
+                        // Save cancelled Temp Targets in TempTargetRunStored Entity
+                        // Cancel ALL active Temp Targets
+                        await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
+
+                        // Update View
+                        state.updateLatestTempTargetConfiguration()
+                    }
+                }, label: {
+                    Text("Stop Temp Target")
+
+                })
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .disabled(!state.isTempTargetEnabled)
+                    .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
+                    .tint(.white)
+            }
+        }
+
+        private func tempTargetView(
+            for tempTarget: TempTargetStored,
+            showCheckmark: Bool = false,
+            onTap: (() -> Void)? = nil
+        ) -> some View {
+            let target = tempTarget.target ?? 100
+            let tempTargetValue = Decimal(target as! Double.RawValue)
+            let isSelected = tempTarget.id?.uuidString == selectedPresetID
+            let tempTargetHalfBasal = Decimal(
+                tempTarget.halfBasalTarget as? Double
+                    .RawValue ?? Double(state.settingHalfBasalTarget)
+            )
+            let percentage = Int(
+                state.computeAdjustedPercentage(usingHBT: tempTargetHalfBasal, usingTarget: tempTargetValue)
+            )
+            let remainingTime = tempTarget.date?.timeIntervalSinceNow ?? 0
+
+            return ZStack(alignment: .trailing) {
+                HStack {
+                    VStack(alignment: .leading) {
+                        HStack {
+                            Text(tempTarget.name ?? "")
+                            Spacer()
+                            if remainingTime > 0 {
+                                Text("Starts in \(formattedTimeRemaining(remainingTime))")
+                                    .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                            }
+                        }
+                        HStack(spacing: 2) {
+                            Text(formattedGlucose(glucose: target as Decimal))
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                            Text("for")
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                            Text("\(Formatter.integerFormatter.string(from: (tempTarget.duration ?? 0) as NSNumber)!)")
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                            Text("min")
+                                .foregroundColor(.secondary)
+                                .font(.caption)
+                            if state.isAdjustSensEnabled(usingTarget: tempTargetValue) {
+                                Text(", \(percentage)%")
+                                    .foregroundColor(.secondary)
+                                    .font(.caption)
+                            }
+                            Spacer()
+                        }
+                        .padding(.top, 2)
+                    }
+                    .contentShape(Rectangle())
+                    .onTapGesture {
+                        onTap?()
+                    }
+                }
+                if showCheckmark && isSelected {
+                    Image(systemName: "checkmark.circle.fill")
+                        .imageScale(.large)
+                        .fontWeight(.bold)
+                        .foregroundStyle(Color.green)
+                } else if onTap != nil {
+                    Image(systemName: "line.3.horizontal")
+                        .imageScale(.medium)
+                        .foregroundStyle(.secondary)
+                }
+            }
+        }
+
+        private func formattedTimeRemaining(_ timeInterval: TimeInterval) -> String {
+            let totalSeconds = Int(timeInterval)
+            let hours = totalSeconds / 3600
+            let minutes = (totalSeconds % 3600) / 60
+            let seconds = totalSeconds % 60
+
+            if hours > 0 {
+                return "\(hours)h \(minutes)m \(seconds)s"
+            } else if minutes > 0 {
+                return "\(minutes)m \(seconds)s"
+            } else {
+                return "<1m"
+            }
+        }
+
+        private var overrideLabelDivider: some View {
+            Divider()
+                .frame(width: 1, height: 20)
+        }
+
+        @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
+            let isSelected = preset.id == selectedPresetID
+            let name = preset.name ?? ""
+            let indefinite = preset.indefinite
+            let duration = preset.duration?.decimalValue ?? Decimal(0)
+            let percentage = preset.percentage
+            let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
+            let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
+
+            let target: String = {
+                guard let targetValue = preset.target, targetValue != 0 else { return "" }
+                return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
+            }()
+
+            let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
+
+            let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
+
+            let scheduledSMBString: String = {
+                guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
+                return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
+            }()
+
+            let smbString: String = {
+                guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
+                return "SMBs Off\(scheduledSMBString)"
+            }()
+
+            let maxSmbMinsString: String = {
+                guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
+                      smbMinutes != state.defaultSmbMinutes else { return "" }
+                return "\(smbMinutes.formatted()) min SMB"
+            }()
+
+            let maxUamMinsString: String = {
+                guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
+                      uamMinutes != state.defaultUamMinutes else { return "" }
+                return "\(uamMinutes.formatted()) min UAM"
+            }()
+
+            let isfAndCrString: String = {
+                switch (preset.isfAndCr, preset.isf, preset.cr) {
+                case (_, true, true),
+                     (true, _, _):
+                    return " ISF/CR"
+                case (false, true, false):
+                    return " ISF"
+                case (false, false, true):
+                    return " CR"
+                default:
+                    return ""
+                }
+            }()
+
+            let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
+
+            // Combine all labels into a single array, filtering out empty strings
+            let labels: [String] = [
+                durationString,
+                percentageString,
+                targetString,
+                smbString,
+                maxSmbMinsString,
+                maxUamMinsString
+            ].filter { !$0.isEmpty }
+
+            if !name.isEmpty {
+                ZStack(alignment: .trailing) {
+                    HStack {
+                        VStack {
+                            HStack {
+                                Text(name)
+                                Spacer()
+                            }
+                            HStack(spacing: 5) {
+                                ForEach(labels, id: \.self) { label in
+                                    Text(label)
+                                    if label != labels.last { // Add divider between labels
+                                        overrideLabelDivider
+                                    }
+                                }
+                                Spacer()
+                            }
+                            .padding(.top, 2)
+                            .foregroundColor(.secondary)
+                            .font(.caption)
+                        }
+                        .contentShape(Rectangle())
+                        .onTapGesture {
+                            Task {
+                                let objectID = preset.objectID
+                                await state.enactOverridePreset(withID: objectID)
+                                state.hideModal()
+                                showCheckmark.toggle()
+                                selectedPresetID = preset.id
+
+                                // Deactivate checkmark after 3 seconds
+                                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+                                    showCheckmark = false
+                                }
+                            }
+                        }
+                    }
+                    // show checkmark to indicate if the preset was actually pressed
+                    if showCheckmark && isSelected {
+                        Image(systemName: "checkmark.circle.fill")
+                            .imageScale(.large)
+                            .fontWeight(.bold)
+                            .foregroundStyle(Color.green)
+                    } else {
+                        Image(systemName: "line.3.horizontal")
+                            .imageScale(.medium)
+                            .foregroundStyle(.secondary)
+                    }
+                }
+            }
+        }
+    }
+}

+ 474 - 0
FreeAPS/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift

@@ -0,0 +1,474 @@
+import Foundation
+import SwiftUI
+
+struct AddOverrideForm: View {
+    @Environment(\.presentationMode) var presentationMode
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(\.dismiss) var dismiss
+    @Environment(AppState.self) var appState
+    @Bindable var state: Adjustments.StateModel
+    @State private var selectedIsfCrOption: IsfAndOrCrOptions = .isfAndCr
+    @State private var selectedDisableSmbOption: DisableSmbOptions = .dontDisable
+    @State private var percentageStep: Int = 5
+    @State private var displayPickerPercentage: Bool = false
+    @State private var displayPickerDuration: Bool = false
+    @State private var targetStep: Decimal = 5
+    @State private var displayPickerTarget: Bool = false
+    @State private var displayPickerDisableSmbSchedule: Bool = false
+    @State private var displayPickerSmbMinutes: Bool = false
+    @State private var durationHours = 0
+    @State private var durationMinutes = 0
+    @State private var overrideTarget = false
+    @State private var didPressSave = false
+
+    var body: some View {
+        NavigationView {
+            List {
+                addOverride()
+                saveButton
+            }
+            .listSectionSpacing(10)
+            .padding(.top, 30)
+            .ignoresSafeArea(edges: .top)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Add Override")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button(action: {
+                        presentationMode.wrappedValue.dismiss()
+                    }, label: {
+                        Text("Cancel")
+                    })
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(
+                        action: {
+                            state.isHelpSheetPresented.toggle()
+                        },
+                        label: {
+                            Image(systemName: "questionmark.circle")
+                        }
+                    )
+                }
+            }
+            .onAppear { targetStep = state.units == .mgdL ? 5 : 9 }
+            .sheet(isPresented: $state.isHelpSheetPresented) {
+                NavigationStack {
+                    List {
+                        Text("Lorem Ipsum Dolor Sit Amet")
+                    }
+                    .padding(.trailing, 10)
+                    .navigationBarTitle("Help", displayMode: .inline)
+
+                    Button { state.isHelpSheetPresented.toggle() }
+                    label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                        .buttonStyle(.bordered)
+                        .padding(.top)
+                }
+                .padding()
+                .presentationDetents(
+                    [.fraction(0.9), .large],
+                    selection: $state.helpSheetDetent
+                )
+            }
+        }
+    }
+
+    @ViewBuilder private func addOverride() -> some View {
+        Group {
+            Section {
+                HStack {
+                    Text("Name")
+                    Spacer()
+                    TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing)
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            Section(footer: state.percentageDescription(state.overridePercentage)) {
+                // Percentage Picker
+                HStack {
+                    Text("Change Basal Rate by")
+                    Spacer()
+                    Text("\(state.overridePercentage.formatted(.number)) %")
+                        .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
+                }
+                .onTapGesture {
+                    displayPickerPercentage = toggleScrollWheel(displayPickerPercentage)
+                }
+
+                if displayPickerPercentage {
+                    HStack {
+                        // Radio buttons and text on the left side
+                        VStack(alignment: .leading) {
+                            // Radio buttons for step iteration
+                            ForEach([1, 5], id: \.self) { step in
+                                RadioButton(isSelected: percentageStep == step, label: "\(step) %") {
+                                    percentageStep = step
+                                    state.overridePercentage = Adjustments.StateModel.roundOverridePercentageToStep(
+                                        state.overridePercentage,
+                                        step
+                                    )
+                                }
+                                .padding(.top, 10)
+                            }
+                        }
+                        .frame(maxWidth: .infinity)
+
+                        Spacer()
+
+                        // Picker on the right side
+                        Picker(
+                            selection: Binding(
+                                get: { Int(truncating: state.overridePercentage as NSNumber) },
+                                set: { state.overridePercentage = Double($0) }
+                            ), label: Text("")
+                        ) {
+                            ForEach(Array(stride(from: 40, through: 150, by: percentageStep)), id: \.self) { percent in
+                                Text("\(percent) %").tag(percent)
+                            }
+                        }
+                        .pickerStyle(WheelPickerStyle())
+                        .frame(maxWidth: .infinity)
+                    }
+                    .frame(maxWidth: .infinity)
+                    .listRowSeparator(.hidden, edges: .top)
+                }
+
+                // Picker for ISF/CR settings
+                Picker("Also Inversely Change", selection: $selectedIsfCrOption) {
+                    ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
+                        Text(option.rawValue).tag(option)
+                    }
+                }
+                .pickerStyle(MenuPickerStyle())
+                .onChange(of: selectedIsfCrOption) { _, newValue in
+                    switch newValue {
+                    case .isfAndCr:
+                        state.isfAndCr = true
+                        state.isf = true
+                        state.cr = true
+                    case .isf:
+                        state.isfAndCr = false
+                        state.isf = true
+                        state.cr = false
+                    case .cr:
+                        state.isfAndCr = false
+                        state.isf = false
+                        state.cr = true
+                    case .nothing:
+                        state.isfAndCr = false
+                        state.isf = false
+                        state.cr = false
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            Section {
+                Toggle(isOn: $state.shouldOverrideTarget) {
+                    Text("Override Target")
+                }
+
+                if state.shouldOverrideTarget {
+                    let settingsProvider = PickerSettingsProvider.shared
+                    let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose)
+                    TargetPicker(
+                        label: "Target Glucose",
+                        selection: Binding(
+                            get: { state.target },
+                            set: { state.target = $0 }
+                        ),
+                        options: settingsProvider.generatePickerValues(
+                            from: glucoseSetting,
+                            units: state.units,
+                            roundMinToStep: true
+                        ),
+                        units: state.units,
+                        targetStep: $targetStep,
+                        displayPickerTarget: $displayPickerTarget,
+                        toggleScrollWheel: toggleScrollWheel
+                    )
+                    .onAppear {
+                        if state.target == 0 {
+                            state.target = 100
+                        }
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            Section {
+                // Picker for ISF/CR settings
+                Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
+                    ForEach(DisableSmbOptions.allCases, id: \.self) { option in
+                        Text(option.rawValue).tag(option)
+                    }
+                }
+                .pickerStyle(MenuPickerStyle())
+                .onChange(of: selectedDisableSmbOption) { _, newValue in
+                    switch newValue {
+                    case .dontDisable:
+                        state.smbIsOff = false
+                        state.smbIsScheduledOff = false
+                    case .disable:
+                        state.smbIsOff = true
+                        state.smbIsScheduledOff = false
+                    case .disableOnSchedule:
+                        state.smbIsOff = false
+                        state.smbIsScheduledOff = true
+                    }
+                }
+
+                if state.smbIsScheduledOff {
+                    // First Hour SMBs Are Disabled
+                    HStack {
+                        Text("From")
+                        Spacer()
+                        Text(
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: state.start as NSNumber))
+                        )
+                        .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+                        Spacer()
+                        Divider().frame(width: 1, height: 20)
+                        Spacer()
+                        Text("To")
+                        Spacer()
+                        Text(
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: state.end as NSNumber))
+                        )
+                        .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+                        Spacer()
+                    }
+                    .onTapGesture {
+                        displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
+                    }
+
+                    if displayPickerDisableSmbSchedule {
+                        HStack {
+                            // From Picker
+                            Picker(selection: Binding(
+                                get: { Int(truncating: state.start as NSNumber) },
+                                set: { state.start = Decimal($0) }
+                            ), label: Text("")) {
+                                ForEach(0 ..< 24, id: \.self) { hour in
+                                    Text(
+                                        state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state
+                                            .convertTo12HourFormat(hour)
+                                    )
+                                    .tag(hour)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+
+                            // To Picker
+                            Picker(selection: Binding(
+                                get: { Int(truncating: state.end as NSNumber) },
+                                set: { state.end = Decimal($0) }
+                            ), label: Text("")) {
+                                ForEach(0 ..< 24, id: \.self) { hour in
+                                    Text(
+                                        state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state
+                                            .convertTo12HourFormat(hour)
+                                    )
+                                    .tag(hour)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                        }
+                        .listRowSeparator(.hidden, edges: .top)
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            if !state.smbIsOff {
+                Section {
+                    Toggle(isOn: $state.advancedSettings) {
+                        Text("Override Max SMB Minutes")
+                    }
+
+                    if state.advancedSettings {
+                        // SMB Minutes Picker
+                        HStack {
+                            Text("SMB")
+                            Spacer()
+                            Text("\(state.smbMinutes.formatted(.number)) min")
+                                .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+                            Spacer()
+                            Divider().frame(width: 1, height: 20)
+                            Spacer()
+                            Text("UAM")
+                            Spacer()
+                            Text("\(state.uamMinutes.formatted(.number)) min")
+                                .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+                        }
+                        .onTapGesture {
+                            displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
+                        }
+
+                        if displayPickerSmbMinutes {
+                            HStack {
+                                Picker(selection: Binding(
+                                    get: { Int(truncating: state.smbMinutes as NSNumber) },
+                                    set: { state.smbMinutes = Decimal($0) }
+                                ), label: Text("")) {
+                                    ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
+                                        Text("\(minute) min").tag(minute)
+                                    }
+                                }
+                                .pickerStyle(WheelPickerStyle())
+                                .frame(maxWidth: .infinity)
+
+                                Picker(selection: Binding(
+                                    get: { Int(truncating: state.uamMinutes as NSNumber) },
+                                    set: { state.uamMinutes = Decimal($0) }
+                                ), label: Text("")) {
+                                    ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
+                                        Text("\(minute) min").tag(minute)
+                                    }
+                                }
+                                .pickerStyle(WheelPickerStyle())
+                                .frame(maxWidth: .infinity)
+                            }
+                            .listRowSeparator(.hidden, edges: .top)
+                        }
+                    }
+                }
+                .listRowBackground(Color.chart)
+            }
+
+            Section {
+                Toggle(isOn: $state.indefinite) {
+                    Text("Enable Indefinitely")
+                }
+
+                if !state.indefinite {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        Text(state.formatHrMin(Int(state.overrideDuration)))
+                            .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
+                    }
+                    .onTapGesture {
+                        displayPickerDuration = toggleScrollWheel(displayPickerDuration)
+                    }
+
+                    if displayPickerDuration {
+                        HStack {
+                            Picker("Hours", selection: $durationHours) {
+                                ForEach(0 ..< 24) { hour in
+                                    Text("\(hour) hr").tag(hour)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                            .onChange(of: durationHours) {
+                                state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes)
+                            }
+
+                            Picker("Minutes", selection: $durationMinutes) {
+                                ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
+                                    Text("\(minute) min").tag(minute)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                            .onChange(of: durationMinutes) {
+                                state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes)
+                            }
+                        }
+                        .listRowSeparator(.hidden, edges: .top)
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+        }
+    }
+
+    private var saveButton: some View {
+        let (isInvalid, errorMessage) = isOverrideInvalid()
+
+        return Group {
+            Section(
+                header:
+                HStack {
+                    Spacer()
+                    Text(errorMessage ?? "").textCase(nil)
+                        .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                    Spacer()
+                },
+                content: {
+                    Button(action: {
+                        Task {
+                            if state.indefinite { state.overrideDuration = 0 }
+                            state.isEnabled.toggle()
+                            await state.saveCustomOverride()
+                            await state.resetStateVariables()
+                            dismiss()
+                        }
+                    }, label: {
+                        Text("Start Override")
+                    })
+                        .disabled(isInvalid)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                }
+            ).listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
+
+            Section {
+                Button(action: {
+                    Task {
+                        await state.saveOverridePreset()
+                        dismiss()
+                    }
+                }, label: {
+                    Text("Save as Preset")
+
+                })
+                    .disabled(isInvalid)
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .tint(.white)
+            }
+            .listRowBackground(
+                isInvalid ? Color(.systemGray4) : Color.secondary
+            )
+        }
+    }
+
+    private func toggleScrollWheel(_ toggle: Bool) -> Bool {
+        displayPickerDuration = false
+        displayPickerPercentage = false
+        displayPickerTarget = false
+        displayPickerDisableSmbSchedule = false
+        displayPickerSmbMinutes = false
+        return !toggle
+    }
+
+    private func isOverrideInvalid() -> (Bool, String?) {
+        let noDurationSpecified = !state.indefinite && state.overrideDuration == 0
+        let targetZeroWithOverride = state.shouldOverrideTarget && state.target == 0
+        let allSettingsDefault = state.overridePercentage == 100 && !state.shouldOverrideTarget &&
+            !state.advancedSettings && !state.smbIsOff && !state.smbIsScheduledOff
+
+        if noDurationSpecified {
+            return (true, "Enable indefinitely or set a duration.")
+        }
+
+        if targetZeroWithOverride {
+            return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).")
+        }
+
+        if allSettingsDefault {
+            return (true, "All settings are at default values.")
+        }
+
+        return (false, nil)
+    }
+}

+ 635 - 0
FreeAPS/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift

@@ -0,0 +1,635 @@
+import Foundation
+import SwiftUI
+
+struct EditOverrideForm: View {
+    var override: OverrideStored
+    @Environment(\.presentationMode) var presentationMode
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+    @Bindable var state: Adjustments.StateModel
+
+    @State private var name: String
+    @State private var percentage: Double
+    @State private var indefinite: Bool
+    @State private var duration: Decimal
+    @State private var target: Decimal?
+    @State private var advancedSettings: Bool
+    @State private var smbIsOff: Bool
+    @State private var smbIsScheduledOff: Bool
+    @State private var start: Decimal?
+    @State private var end: Decimal?
+    @State private var isfAndCr: Bool
+    @State private var isf: Bool
+    @State private var cr: Bool
+    @State private var smbMinutes: Decimal?
+    @State private var uamMinutes: Decimal?
+    @State private var selectedIsfCrOption: IsfAndOrCrOptions
+    @State private var selectedDisableSmbOption: DisableSmbOptions
+    @State private var hasChanges = false
+    @State private var isEditing = false
+    @State private var target_override = false
+    @State private var percentageStep: Int = 1
+    @State private var displayPickerPercentage: Bool = false
+    @State private var displayPickerDuration: Bool = false
+    @State private var targetStep: Decimal = 1
+    @State private var displayPickerTarget: Bool = false
+    @State private var displayPickerDisableSmbSchedule: Bool = false
+    @State private var displayPickerSmbMinutes: Bool = false
+
+    init(overrideToEdit: OverrideStored, state: Adjustments.StateModel) {
+        override = overrideToEdit
+        _state = Bindable(wrappedValue: state)
+        _name = State(initialValue: overrideToEdit.name ?? "")
+        _percentage = State(initialValue: overrideToEdit.percentage)
+        _indefinite = State(initialValue: overrideToEdit.indefinite)
+        _duration = State(initialValue: overrideToEdit.duration?.decimalValue ?? 0)
+        _target = State(initialValue: overrideToEdit.target?.decimalValue)
+        _target_override = State(initialValue: overrideToEdit.target != nil && overrideToEdit.target?.decimalValue != 0)
+        _advancedSettings = State(initialValue: overrideToEdit.advancedSettings)
+        _smbIsOff = State(initialValue: overrideToEdit.smbIsOff)
+        _smbIsScheduledOff = State(initialValue: overrideToEdit.smbIsScheduledOff)
+        _start = State(initialValue: overrideToEdit.start?.decimalValue)
+        _end = State(initialValue: overrideToEdit.end?.decimalValue)
+        _isfAndCr = State(initialValue: overrideToEdit.isfAndCr)
+        _isf = State(initialValue: overrideToEdit.isf)
+        _cr = State(initialValue: overrideToEdit.cr)
+        _selectedIsfCrOption = State(
+            initialValue: overrideToEdit.isfAndCr ? .isfAndCr
+                : (overrideToEdit.isf ? .isf : (overrideToEdit.cr ? .cr : .nothing))
+        )
+        _selectedDisableSmbOption = State(
+            initialValue: overrideToEdit.smbIsScheduledOff ? .disableOnSchedule
+                : (overrideToEdit.smbIsOff ? .disable : .dontDisable)
+        )
+        _smbMinutes = State(initialValue: overrideToEdit.smbMinutes?.decimalValue)
+        _uamMinutes = State(initialValue: overrideToEdit.uamMinutes?.decimalValue)
+    }
+
+    private var percentageSelection: Binding<Double> {
+        Binding<Double>(
+            get: {
+                let value = floor(percentage / Double(percentageStep)) * Double(percentageStep)
+                return max(10, min(value, 200))
+            },
+            set: {
+                percentage = $0
+                hasChanges = true
+            }
+        )
+    }
+
+    var body: some View {
+        NavigationView {
+            List {
+                editOverride()
+                saveButton
+            }
+            .listSectionSpacing(10)
+            .padding(.top, 30)
+            .ignoresSafeArea(edges: .top)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Edit Override")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button(action: {
+                        presentationMode.wrappedValue.dismiss()
+                    }, label: {
+                        Text("Cancel")
+                    })
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(
+                        action: {
+                            state.isHelpSheetPresented.toggle()
+                        },
+                        label: {
+                            Image(systemName: "questionmark.circle")
+                        }
+                    )
+                }
+            }
+            .onDisappear {
+                if !hasChanges {
+                    // Reset UI changes
+                    resetValues()
+                }
+            }
+            .sheet(isPresented: $state.isHelpSheetPresented) {
+                NavigationStack {
+                    List {
+                        Text("Lorem Ipsum Dolor Sit Amet")
+                    }
+                    .padding(.trailing, 10)
+                    .navigationBarTitle("Help", displayMode: .inline)
+
+                    Button { state.isHelpSheetPresented.toggle() }
+                    label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                        .buttonStyle(.bordered)
+                        .padding(.top)
+                }
+                .padding()
+                .presentationDetents(
+                    [.fraction(0.9), .large],
+                    selection: $state.helpSheetDetent
+                )
+            }
+        }
+    }
+
+    @ViewBuilder private func editOverride() -> some View {
+        Group {
+            if override.name != nil {
+                Section {
+                    HStack {
+                        Text("Name")
+                        Spacer()
+                        TextField("Name", text: $name)
+                            .onChange(of: name) { hasChanges = true }
+                            .multilineTextAlignment(.trailing)
+                    }
+                }
+                .listRowBackground(Color.chart)
+            }
+
+            // Percentage Picker
+            Section(footer: state.percentageDescription(percentage)) {
+                HStack {
+                    Text("Change Basal Rate by")
+                    Spacer()
+                    Text("\(percentage.formatted(.number)) %")
+                        .foregroundColor(!displayPickerPercentage ? .primary : .accentColor)
+                }
+                .onTapGesture {
+                    displayPickerPercentage = toggleScrollWheel(displayPickerPercentage)
+                }
+
+                if displayPickerPercentage {
+                    HStack {
+                        // Radio buttons and text on the left side
+                        VStack(alignment: .leading) {
+                            // Radio buttons for step iteration
+                            ForEach([1, 5], id: \.self) { step in
+                                RadioButton(isSelected: percentageStep == step, label: "\(step) %") {
+                                    percentageStep = step
+                                    percentage = Adjustments.StateModel.roundOverridePercentageToStep(percentage, step)
+                                }
+                                .padding(.top, 10)
+                            }
+                        }
+                        .frame(maxWidth: .infinity)
+
+                        Spacer()
+
+                        // Picker on the right side
+                        Picker(
+                            selection: percentageSelection,
+                            label: Text("")
+                        ) {
+                            ForEach(
+                                Array(stride(from: 40.0, through: 150.0, by: Double(percentageStep))),
+                                id: \.self
+                            ) { percent in
+                                Text("\(Int(percent)) %").tag(percent)
+                            }
+                        }
+                        .pickerStyle(WheelPickerStyle())
+                        .frame(maxWidth: .infinity)
+                    }
+                    .listRowSeparator(.hidden, edges: .top)
+                }
+
+                // Picker for ISF/CR settings
+                Picker("Also Change", selection: $selectedIsfCrOption) {
+                    ForEach(IsfAndOrCrOptions.allCases, id: \.self) { option in
+                        Text(option.rawValue).tag(option)
+                    }
+                }
+                .pickerStyle(MenuPickerStyle())
+                .onChange(of: selectedIsfCrOption) { _, newValue in
+                    switch newValue {
+                    case .isfAndCr:
+                        isfAndCr = true
+                        isf = false
+                        cr = false
+                    case .isf:
+                        isfAndCr = false
+                        isf = true
+                        cr = false
+                    case .cr:
+                        isfAndCr = false
+                        isf = false
+                        cr = true
+                    case .nothing:
+                        isfAndCr = false
+                        isf = false
+                        cr = false
+                    }
+                    hasChanges = true
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            Section {
+                Toggle(isOn: $target_override) {
+                    Text("Override Target")
+                }
+                .onChange(of: target_override) {
+                    hasChanges = true
+                }
+                // Target Glucose Picker
+                if target_override {
+                    let settingsProvider = PickerSettingsProvider.shared
+                    let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 72, max: 270, type: .glucose)
+
+                    TargetPicker(
+                        label: "Target Glucose",
+                        selection: Binding(
+                            get: { target ?? 100 },
+                            set: { target = $0 }
+                        ),
+                        options: settingsProvider.generatePickerValues(
+                            from: glucoseSetting,
+                            units: state.units,
+                            roundMinToStep: true
+                        ),
+                        units: state.units,
+                        hasChanges: $hasChanges,
+                        targetStep: $targetStep,
+                        displayPickerTarget: $displayPickerTarget,
+                        toggleScrollWheel: toggleScrollWheel
+                    )
+                    .onAppear {
+                        if target == 0 || target == nil {
+                            target = 100
+                        }
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            Section {
+                // Picker for Disable SMB settings
+                Picker("Disable SMBs", selection: $selectedDisableSmbOption) {
+                    ForEach(DisableSmbOptions.allCases, id: \.self) { option in
+                        Text(option.rawValue).tag(option)
+                    }
+                }
+                .pickerStyle(MenuPickerStyle())
+                .onChange(of: selectedDisableSmbOption) { _, newValue in
+                    switch newValue {
+                    case .dontDisable:
+                        smbIsOff = false
+                        smbIsScheduledOff = false
+                    case .disable:
+                        smbIsOff = true
+                        smbIsScheduledOff = false
+                    case .disableOnSchedule:
+                        smbIsOff = false
+                        smbIsScheduledOff = true
+                    }
+                    hasChanges = true
+                }
+
+                if smbIsScheduledOff {
+                    // First Hour SMBs Are Disabled
+                    HStack {
+                        Text("From")
+                        Spacer()
+                        Text(
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: start! as NSNumber))
+                        )
+                        .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+
+                        Spacer()
+
+                        Divider().frame(width: 1, height: 20)
+
+                        Spacer()
+
+                        Text("To")
+                        Spacer()
+                        Text(
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: end! as NSNumber))
+                        )
+                        .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
+                    }
+                    .onTapGesture {
+                        displayPickerDisableSmbSchedule = toggleScrollWheel(displayPickerDisableSmbSchedule)
+                    }
+
+                    if displayPickerDisableSmbSchedule {
+                        HStack {
+                            Picker(selection: Binding(
+                                get: { Int(truncating: start! as NSNumber) },
+                                set: {
+                                    start = Decimal($0)
+                                    hasChanges = true
+                                }
+                            ), label: Text("")) {
+                                if state.is24HourFormat() {
+                                    ForEach(0 ..< 24, id: \.self) { hour in
+                                        Text(state.format24Hour(hour) + ":00").tag(hour)
+                                    }
+                                } else {
+                                    ForEach(0 ..< 24, id: \.self) { hour in
+                                        Text(state.convertTo12HourFormat(hour)).tag(hour)
+                                    }
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+
+                            Picker(selection: Binding(
+                                get: { Int(truncating: end! as NSNumber) },
+                                set: {
+                                    end = Decimal($0)
+                                    hasChanges = true
+                                }
+                            ), label: Text("")) {
+                                if state.is24HourFormat() {
+                                    ForEach(0 ..< 24, id: \.self) { hour in
+                                        Text(state.format24Hour(hour) + ":00").tag(hour)
+                                    }
+                                } else {
+                                    ForEach(0 ..< 24, id: \.self) { hour in
+                                        Text(state.convertTo12HourFormat(hour)).tag(hour)
+                                    }
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                        }
+                        .listRowSeparator(.hidden, edges: .top)
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            if !smbIsOff {
+                Section {
+                    Toggle(isOn: $advancedSettings) {
+                        Text("Change Max SMB Minutes")
+                    }
+                    .onChange(of: advancedSettings) { hasChanges = true }
+
+                    if advancedSettings {
+                        // SMB Minutes Picker
+                        HStack {
+                            Text("SMB")
+                            Spacer()
+                            Text("\(smbMinutes?.formatted(.number) ?? "\(state.defaultSmbMinutes)") min")
+                                .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+
+                            Spacer()
+
+                            Divider().frame(width: 1, height: 20)
+
+                            Spacer()
+
+                            Text("UAM")
+                            Spacer()
+                            Text("\(uamMinutes?.formatted(.number) ?? "\(state.defaultUamMinutes)") min")
+                                .foregroundColor(!displayPickerSmbMinutes ? .primary : .accentColor)
+                        }
+                        .onTapGesture {
+                            displayPickerSmbMinutes = toggleScrollWheel(displayPickerSmbMinutes)
+                        }
+
+                        if displayPickerSmbMinutes {
+                            HStack {
+                                Picker(
+                                    selection: Binding(
+                                        get: { smbMinutes ?? state.defaultSmbMinutes },
+                                        set: {
+                                            smbMinutes = $0
+                                            hasChanges = true
+                                        }
+                                    ),
+                                    label: Text("")
+                                ) {
+                                    ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
+                                        Text("\(minute) min").tag(Decimal(minute))
+                                    }
+                                }
+                                .pickerStyle(WheelPickerStyle())
+                                .frame(maxWidth: .infinity)
+
+                                Picker(
+                                    selection: Binding(
+                                        get: { uamMinutes ?? state.defaultUamMinutes },
+                                        set: {
+                                            uamMinutes = $0
+                                            hasChanges = true
+                                        }
+                                    ),
+                                    label: Text("")
+                                ) {
+                                    ForEach(Array(stride(from: 0, through: 180, by: 5)), id: \.self) { minute in
+                                        Text("\(minute) min").tag(Decimal(minute))
+                                    }
+                                }
+                                .pickerStyle(WheelPickerStyle())
+                                .frame(maxWidth: .infinity)
+                            }
+                            .listRowSeparator(.hidden, edges: .top)
+                        }
+                    }
+                }
+                .listRowBackground(Color.chart)
+            }
+
+            Section {
+                Toggle(isOn: $indefinite) { Text("Enable Indefinitely") }
+                    .onChange(of: indefinite) { hasChanges = true }
+
+                if !indefinite {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        Text(state.formatHrMin(Int(truncating: duration as NSNumber)))
+                            .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
+                    }
+                    .onTapGesture {
+                        displayPickerDuration = toggleScrollWheel(displayPickerDuration)
+                    }
+
+                    if displayPickerDuration {
+                        HStack {
+                            Picker(
+                                selection: Binding(
+                                    get: {
+                                        Int(truncating: duration as NSNumber) / 60
+                                    },
+                                    set: {
+                                        let minutes = Int(truncating: duration as NSNumber) % 60
+                                        let totalMinutes = $0 * 60 + minutes
+                                        duration = Decimal(totalMinutes)
+                                        hasChanges = true
+                                    }
+                                ),
+                                label: Text("")
+                            ) {
+                                ForEach(0 ..< 24) { hour in
+                                    Text("\(hour) hr").tag(hour)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+
+                            Picker(
+                                selection: Binding(
+                                    get: {
+                                        Int(truncating: duration as NSNumber) %
+                                            60 // Convert Decimal to Int for modulus operation
+                                    },
+                                    set: {
+                                        duration = Decimal((Int(truncating: duration as NSNumber) / 60) * 60 + $0)
+                                        hasChanges = true
+                                    }
+                                ),
+                                label: Text("")
+                            ) {
+                                ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
+                                    Text("\(minute) min").tag(minute)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                        }
+                        .listRowSeparator(.hidden, edges: .top)
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+        }
+    }
+
+    private var saveButton: some View {
+        let (isInvalid, errorMessage) = isOverrideInvalid()
+
+        return Section(
+            header:
+            HStack {
+                Spacer()
+                Text(errorMessage ?? "").textCase(nil)
+                    .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                Spacer()
+            },
+            content: {
+                Button(action: {
+                    saveChanges()
+
+                    do {
+                        guard let moc = override.managedObjectContext else { return }
+                        guard moc.hasChanges else { return }
+                        try moc.save()
+                        Task {
+                            await state.nightscoutManager.uploadProfiles()
+                        }
+                        // Disable previous active Override
+                        if let currentActiveOverride = state.currentActiveOverride {
+                            Task {
+                                await state.disableAllActiveOverrides(
+                                    except: currentActiveOverride.objectID,
+                                    createOverrideRunEntry: false
+                                )
+                                // Update View
+                                state.updateLatestOverrideConfiguration()
+                            }
+                        }
+
+                        hasChanges = false
+                        presentationMode.wrappedValue.dismiss()
+                    } catch {
+                        debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to edit Override")
+                    }
+                }, label: {
+                    Text("Save Override")
+                })
+                    .disabled(isInvalid) // Disable button if changes are invalid
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .tint(.white)
+            }
+        )
+        .listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
+    }
+
+    private func isOverrideInvalid() -> (Bool, String?) {
+        let noDurationSpecified = !indefinite && duration == 0
+        let targetZeroWithOverride = target_override && (target ?? 0 < 72 || target ?? 0 > 270)
+        let allSettingsDefault = percentage == 100 && !target_override && !advancedSettings &&
+            !smbIsOff && !smbIsScheduledOff
+
+        if noDurationSpecified {
+            return (true, "Enable indefinitely or set a duration.")
+        }
+
+        if targetZeroWithOverride {
+            return (true, "Target glucose is out of range (\(state.units == .mgdL ? "72-270" : "4-14")).")
+        }
+
+        if allSettingsDefault {
+            return (true, "All settings are at default values.")
+        }
+
+        if !hasChanges {
+            return (true, nil)
+        }
+
+        return (false, nil)
+    }
+
+    private func saveChanges() {
+        if !override.isPreset, hasChanges, name == (override.name ?? "") {
+            override.name = "Custom Override"
+        } else {
+            override.name = name
+        }
+        override.percentage = percentage
+        override.indefinite = indefinite
+        override.duration = NSDecimalNumber(decimal: duration)
+        override.target = target_override ? NSDecimalNumber(decimal: target ?? 100) : nil
+        override.advancedSettings = advancedSettings
+        override.smbIsOff = smbIsOff
+        override.smbIsScheduledOff = smbIsScheduledOff
+        override.start = start.map { NSDecimalNumber(decimal: $0) }
+        override.end = end.map { NSDecimalNumber(decimal: $0) }
+        override.isfAndCr = isfAndCr
+        override.isf = isf
+        override.cr = cr
+        override.smbMinutes = smbMinutes.map { NSDecimalNumber(decimal: $0) }
+        override.uamMinutes = uamMinutes.map { NSDecimalNumber(decimal: $0) }
+        override.isUploadedToNS = false
+    }
+
+    private func resetValues() {
+        name = override.name ?? ""
+        percentage = override.percentage
+        indefinite = override.indefinite
+        duration = override.duration?.decimalValue ?? 0
+        target = override.target?.decimalValue
+        advancedSettings = override.advancedSettings
+        smbIsOff = override.smbIsOff
+        smbIsScheduledOff = override.smbIsScheduledOff
+        start = override.start?.decimalValue
+        end = override.end?.decimalValue
+        isfAndCr = override.isfAndCr
+        isf = override.isf
+        cr = override.cr
+        smbMinutes = override.smbMinutes?.decimalValue ?? state.defaultSmbMinutes
+        uamMinutes = override.uamMinutes?.decimalValue ?? state.defaultUamMinutes
+    }
+
+    private func toggleScrollWheel(_ toggle: Bool) -> Bool {
+        displayPickerDuration = false
+        displayPickerPercentage = false
+        displayPickerTarget = false
+        displayPickerDisableSmbSchedule = false
+        displayPickerSmbMinutes = false
+        return !toggle
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 388 - 0
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift


+ 410 - 0
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift

@@ -0,0 +1,410 @@
+import Foundation
+import SwiftUI
+
+struct EditTempTargetForm: View {
+    @ObservedObject var tempTarget: TempTargetStored
+    @Environment(\.presentationMode) var presentationMode
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+    @StateObject var state: Adjustments.StateModel
+    @State private var displayPickerDuration: Bool = false
+    @State private var displayPickerTarget: Bool = false
+    @State private var tempTargetSensitivityAdjustmentType: TempTargetSensitivityAdjustmentType = .standard
+    @State private var durationHours = 0
+    @State private var durationMinutes = 0
+    @State private var targetStep: Decimal = 1
+    @State private var name: String
+    @State private var target: Decimal
+    @State private var duration: Decimal
+    @State private var date: Date
+    @State private var halfBasalTarget: Decimal?
+    @State private var percentage: Double
+
+    @State private var hasChanges = false
+    @State private var showAlert = false
+    @State private var isUsingSlider = false
+    @State private var isPreset = false
+    @State private var isEnabled = false
+
+    init(tempTargetToEdit: TempTargetStored, state: Adjustments.StateModel) {
+        tempTarget = tempTargetToEdit
+        _state = StateObject(wrappedValue: state)
+        _name = State(initialValue: tempTargetToEdit.name ?? "")
+        _target = State(initialValue: tempTargetToEdit.target?.decimalValue ?? 0)
+        _duration = State(initialValue: tempTargetToEdit.duration?.decimalValue ?? 0)
+        _date = State(initialValue: tempTargetToEdit.date ?? Date())
+        _halfBasalTarget = State(initialValue: tempTargetToEdit.halfBasalTarget?.decimalValue ?? state.settingHalfBasalTarget)
+        _isPreset = State(initialValue: tempTargetToEdit.isPreset)
+        _isEnabled = State(initialValue: tempTargetToEdit.enabled)
+
+        let tempTargetHalfBasal: Decimal = (tempTargetToEdit.halfBasalTarget?.decimalValue) ?? state.settingHalfBasalTarget
+
+        let H = tempTargetHalfBasal
+        let T = tempTargetToEdit.target?.decimalValue ?? 100
+        let calcPercentage = state.computeAdjustedPercentage(usingHBT: H, usingTarget: T)
+        _percentage = State(initialValue: calcPercentage)
+    }
+
+    private var dateFormatter: DateFormatter {
+        let f = DateFormatter()
+        f.dateStyle = .short
+        f.timeStyle = .short
+        return f
+    }
+
+    var body: some View {
+        NavigationView {
+            List {
+                editTempTarget()
+                saveButton
+            }
+            .listSectionSpacing(10)
+            .padding(.top, 30)
+            .ignoresSafeArea(edges: .top)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .navigationTitle("Edit Temp Target")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button(action: {
+                        presentationMode.wrappedValue.dismiss()
+                    }, label: {
+                        Text("Cancel")
+                    })
+                }
+            }
+            .onAppear {
+                if halfBasalTarget != state.settingHalfBasalTarget { tempTargetSensitivityAdjustmentType = .slider }
+            }
+        }
+    }
+
+    private func calculatedEndDate(from startDate: Date, totalDuration: Decimal) -> Date {
+        let elapsedTime = Date().timeIntervalSince(startDate)
+        let totalDurationSeconds = Int(totalDuration) * 60
+        let remainingTime = max(totalDurationSeconds - Int(elapsedTime), 0)
+        return Date().addingTimeInterval(TimeInterval(remainingTime))
+    }
+
+    private func formattedEndTime(startDate: Date, totalDuration: Decimal) -> String {
+        let endDate = calculatedEndDate(from: startDate, totalDuration: totalDuration)
+        let formatter = DateFormatter()
+
+        if Calendar.current.isDateInToday(endDate) {
+            formatter.dateStyle = .none
+            formatter.timeStyle = .short // show only the time
+        } else {
+            formatter.dateStyle = .short
+            formatter.timeStyle = .short // show Date and time
+        }
+
+        return formatter.string(from: endDate)
+    }
+
+    @ViewBuilder private func editTempTarget() -> some View {
+        Group {
+            Section {
+                HStack {
+                    Text("Name")
+                    Spacer()
+                    TextField("(Optional)", text: $name)
+                        .multilineTextAlignment(.trailing)
+                        .onChange(of: name) {
+                            hasChanges = true
+                        }
+                }
+            }.listRowBackground(Color.chart)
+
+            Section {
+                // Picker on the right side
+                let settingsProvider = PickerSettingsProvider.shared
+                let glucoseSetting = PickerSetting(value: 0, step: targetStep, min: 80, max: 200, type: .glucose)
+                TargetPicker(
+                    label: "Target Glucose",
+                    selection: Binding(
+                        get: { target },
+                        set: { target = $0 }
+                    ),
+                    options: settingsProvider.generatePickerValues(
+                        from: glucoseSetting,
+                        units: state.units,
+                        roundMinToStep: true
+                    ),
+                    units: state.units,
+                    hasChanges: $hasChanges,
+                    targetStep: $targetStep,
+                    displayPickerTarget: $displayPickerTarget,
+                    toggleScrollWheel: toggleScrollWheel
+                )
+                .onChange(of: target) {
+                    percentage = state.computeAdjustedPercentage(usingHBT: halfBasalTarget, usingTarget: target)
+                }
+            }
+            .listRowBackground(Color.chart)
+
+            if target != state.normalTarget {
+                let computedHalfBasalTarget = Decimal(
+                    state
+                        .computeHalfBasalTarget(usingTarget: target, usingPercentage: percentage)
+                )
+
+                if state.isAdjustSensEnabled(usingTarget: target) {
+                    Section(
+                        footer: state.percentageDescription(percentage),
+                        content: {
+                            Picker("Sensitivity Adjustment", selection: $tempTargetSensitivityAdjustmentType) {
+                                ForEach(TempTargetSensitivityAdjustmentType.allCases, id: \.self) { option in
+                                    Text(option.rawValue).tag(option)
+                                }
+                                .pickerStyle(MenuPickerStyle())
+                                .onChange(of: tempTargetSensitivityAdjustmentType) { _, newValue in
+                                    if newValue == .standard {
+                                        halfBasalTarget = nil
+                                        hasChanges = true
+                                        percentage = state.computeAdjustedPercentage(
+                                            usingHBT: halfBasalTarget,
+                                            usingTarget: target
+                                        )
+                                    }
+                                }
+                            }
+
+                            Text("\(formattedPercentage(percentage))% Insulin")
+                                .foregroundColor(isUsingSlider ? .orange : Color.tabBar)
+                                .font(.title3)
+                                .fontWeight(.bold)
+                                .frame(maxWidth: .infinity, alignment: .center)
+
+                            if tempTargetSensitivityAdjustmentType == .slider {
+                                Slider(
+                                    value: Binding(
+                                        get: {
+                                            Double(truncating: percentage as NSNumber)
+                                        },
+                                        set: { newValue in
+                                            percentage = newValue
+                                            hasChanges = true
+                                            halfBasalTarget = Decimal(state.computeHalfBasalTarget(
+                                                usingTarget: target,
+                                                usingPercentage: percentage
+                                            ))
+                                        }
+                                    ),
+                                    in: state.computeSliderLow(usingTarget: target) ... state
+                                        .computeSliderHigh(usingTarget: target),
+                                    step: 5
+                                ) {}
+                                minimumValueLabel: {
+                                    Text("\(state.computeSliderLow(usingTarget: target), specifier: "%.0f")%")
+                                }
+                                maximumValueLabel: {
+                                    Text("\(state.computeSliderHigh(usingTarget: target), specifier: "%.0f")%")
+                                }
+                                .listRowSeparator(.hidden, edges: .top)
+                            }
+                        }
+                    )
+                    .listRowBackground(Color.chart)
+                }
+            }
+
+            Section {
+                DatePicker("Start Time", selection: $date, in: Date.now...)
+                    .onChange(of: date) { hasChanges = true }
+            }.listRowBackground(Color.chart)
+
+            Section {
+                VStack {
+                    HStack {
+                        Text("Duration")
+                        Spacer()
+                        Text(state.formatHrMin(Int(duration)))
+                            .foregroundColor(!displayPickerDuration ? (duration > 0 ? .primary : .secondary) : .accentColor)
+                    }
+                    .onTapGesture {
+                        displayPickerDuration = toggleScrollWheel(displayPickerDuration)
+                    }
+
+                    if displayPickerDuration {
+                        HStack {
+                            Picker(
+                                selection: Binding(
+                                    get: {
+                                        Int(truncating: duration as NSNumber) / 60
+                                    },
+                                    set: {
+                                        let minutes = Int(truncating: duration as NSNumber) % 60
+                                        let totalMinutes = $0 * 60 + minutes
+                                        duration = Decimal(totalMinutes)
+                                        hasChanges = duration > 0 ? true : false // prevents the user from setting 0 min
+                                    }
+                                ),
+                                label: Text("")
+                            ) {
+                                ForEach(0 ..< 24) { hour in
+                                    Text("\(hour) hr").tag(hour)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+
+                            Picker(
+                                selection: Binding(
+                                    get: {
+                                        Int(truncating: duration as NSNumber) %
+                                            60 // Convert Decimal to Int for modulus operation
+                                    },
+                                    set: {
+                                        duration = Decimal((Int(truncating: duration as NSNumber) / 60) * 60 + $0)
+                                        hasChanges = duration > 0 ? true : false
+                                    }
+                                ),
+                                label: Text("")
+                            ) {
+                                ForEach(Array(stride(from: 0, through: 55, by: 5)), id: \.self) { minute in
+                                    Text("\(minute) min").tag(minute)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                        }
+                        .listRowSeparator(.hidden, edges: .top)
+                    }
+                }
+            }.listRowBackground(Color.chart)
+
+            if isEnabled {
+                Section {
+                    HStack {
+                        Spacer()
+                        Text("Until \(formattedEndTime(startDate: date, totalDuration: duration))").foregroundStyle(.secondary)
+                    }
+                }.listRowBackground(Color.clear)
+            }
+        }
+    }
+
+    private var saveButton: some View {
+        HStack {
+            Spacer()
+            Button(action: {
+                saveChanges()
+                do {
+                    guard let moc = tempTarget.managedObjectContext else { return }
+                    guard moc.hasChanges else { return }
+                    try moc.save()
+
+                    if let currentActiveTempTarget = state.currentActiveTempTarget {
+                        Task {
+                            // TODO: - Creating a Run entry is probably needed for Overrides as well and the reason for "jumping" Overrides?
+                            // Disable previous active Temp Targets
+                            await state.disableAllActiveOverrides(
+                                except: currentActiveTempTarget.objectID,
+                                createOverrideRunEntry: false
+                            )
+
+                            // If the temp target which currently gets edited is enabled, then store it to the Temp Target JSON so that oref uses it
+                            if isEnabled {
+                                let tempTarget = TempTarget(
+                                    name: name,
+                                    createdAt: Date(),
+                                    targetTop: target,
+                                    targetBottom: target,
+                                    duration: duration,
+                                    enteredBy: TempTarget.local,
+                                    reason: TempTarget.custom,
+                                    isPreset: isPreset ? true : false,
+                                    enabled: isEnabled ? true : false,
+                                    halfBasalTarget: halfBasalTarget
+                                )
+
+                                // Store to TempTargetStorage so that oref uses the edited Temp target
+                                state.saveTempTargetToStorage(tempTargets: [tempTarget])
+                            }
+
+                            // Update view
+                            state.updateLatestTempTargetConfiguration()
+                        }
+                    }
+                    hasChanges = false
+                    presentationMode.wrappedValue.dismiss()
+                } catch {
+                    debugPrint("Failed to Edit Temp Target")
+                }
+            }, label: {
+                Text("Save")
+            })
+                .disabled(!hasChanges)
+                .frame(maxWidth: .infinity, alignment: .center)
+                .tint(.white)
+
+            Spacer()
+        }.listRowBackground(hasChanges ? Color(.systemBlue) : Color(.systemGray4))
+    }
+
+    private func saveChanges() {
+        tempTarget.name = name
+        tempTarget.target = NSDecimalNumber(decimal: target)
+        tempTarget.duration = NSDecimalNumber(decimal: duration)
+        tempTarget.date = date
+        tempTarget.isUploadedToNS = false
+        if let halfBasalValue = halfBasalTarget {
+            tempTarget.halfBasalTarget = NSDecimalNumber(decimal: halfBasalValue)
+        } else {
+            tempTarget.halfBasalTarget = nil
+        }
+    }
+
+    private func toggleScrollWheel(_ toggle: Bool) -> Bool {
+        displayPickerDuration = false
+        displayPickerTarget = false
+        return !toggle
+    }
+
+    private func resetValues() {
+        name = tempTarget.name ?? ""
+        target = tempTarget.target?.decimalValue ?? 0
+        duration = tempTarget.duration?.decimalValue ?? 0
+        date = tempTarget.date ?? Date()
+    }
+
+    private func totalDurationInMinutes() -> Int {
+        let durationTotal = (durationHours * 60) + durationMinutes
+        return max(0, durationTotal)
+    }
+
+    private var formatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        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
+    }
+
+    private func formattedPercentage(_ value: Double) -> String {
+        let percentageNumber = NSNumber(value: value)
+        return formatter.string(from: percentageNumber) ?? "\(value)"
+    }
+
+    private func formattedGlucose(glucose: Decimal) -> String {
+        let formattedValue: String
+        if state.units == .mgdL {
+            formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
+        } else {
+            formattedValue = glucose.formattedAsMmolL
+        }
+        return "\(formattedValue) \(state.units.rawValue)"
+    }
+}

+ 19 - 0
FreeAPS/Sources/Modules/Adjustments/View/ViewElements/RadioButton.swift

@@ -0,0 +1,19 @@
+import SwiftUI
+
+struct RadioButton: View {
+    var isSelected: Bool
+    var label: String
+    var action: () -> Void
+
+    var body: some View {
+        Button(action: {
+            action()
+        }) {
+            HStack {
+                Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
+                Text(label) // Add label inside the button to make it tappable
+            }
+        }
+        .buttonStyle(PlainButtonStyle())
+    }
+}

+ 67 - 0
FreeAPS/Sources/Modules/Adjustments/View/ViewElements/TargetPicker.swift

@@ -0,0 +1,67 @@
+import SwiftUI
+
+struct TargetPicker: View {
+    let label: String
+    @Binding var selection: Decimal
+    let options: [Decimal]
+    let units: GlucoseUnits
+    var hasChanges: Binding<Bool>?
+    @Binding var targetStep: Decimal
+    @Binding var displayPickerTarget: Bool
+    var toggleScrollWheel: (_ picker: Bool) -> Bool
+
+    var body: some View {
+        HStack {
+            Text(label)
+            Spacer()
+            Text(
+                (units == .mgdL ? selection.description : selection.formattedAsMmolL) + " " + units.rawValue
+            )
+            .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
+        }
+        .onTapGesture {
+            displayPickerTarget = toggleScrollWheel(displayPickerTarget)
+        }
+        if displayPickerTarget {
+            HStack {
+                // Radio buttons and text on the left side
+                VStack(alignment: .leading) {
+                    // Radio buttons for step iteration
+                    let stepChoices: [Decimal] = units == .mgdL ? [1, 5] : [1, 9]
+                    ForEach(stepChoices, id: \.self) { step in
+                        let label = (units == .mgdL ? step.description : step.formattedAsMmolL) + " " +
+                            units.rawValue
+                        RadioButton(
+                            isSelected: targetStep == step,
+                            label: label
+                        ) {
+                            targetStep = step
+                            selection = Adjustments.StateModel.roundTargetToStep(selection, step)
+                        }
+                        .padding(.top, 10)
+                    }
+                }
+                .frame(maxWidth: .infinity)
+
+                Spacer()
+
+                // Picker on the right side
+                Picker(selection: Binding(
+                    get: { Adjustments.StateModel.roundTargetToStep(selection, targetStep) },
+                    set: {
+                        selection = $0
+                        hasChanges?.wrappedValue = true // This safely updates if hasChanges is provided
+                    }
+                ), label: Text("")) {
+                    ForEach(options, id: \.self) { option in
+                        Text((units == .mgdL ? option.description : option.formattedAsMmolL) + " " + units.rawValue)
+                            .tag(option)
+                    }
+                }
+                .pickerStyle(WheelPickerStyle())
+                .frame(maxWidth: .infinity)
+            }
+            .listRowSeparator(.hidden, edges: .top)
+        }
+    }
+}

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

@@ -15,6 +15,15 @@ extension AlgorithmAdvancedSettings {
                 ?? PumpSettings(insulinActionCurve: 6.0, maxBolus: 10, maxBasal: 2)
         }
 
+        func savePreferences(_ preferences: Preferences) {
+            storage.save(preferences, as: OpenAPS.Settings.preferences)
+            processQueue.async {
+                self.broadcaster.notify(PreferencesObserver.self, on: self.processQueue) {
+                    $0.preferencesDidChange(preferences)
+                }
+            }
+        }
+
         func save(settings: PumpSettings) -> AnyPublisher<Void, Error> {
             func save(_ settings: PumpSettings) {
                 storage.save(settings, as: OpenAPS.Settings.settings)

+ 32 - 67
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift

@@ -3,32 +3,28 @@ import Observation
 import SwiftUI
 
 extension AlgorithmAdvancedSettings {
-    @Observable final class StateModel: BaseStateModel<Provider> {
-        @ObservationIgnored @Injected() var settings: SettingsManager!
-        @ObservationIgnored @Injected() var storage: FileStorage!
-        @ObservationIgnored @Injected() var nightscout: NightscoutManager!
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Injected() var storage: FileStorage!
+        @Injected() var nightscout: NightscoutManager!
 
         var units: GlucoseUnits = .mgdL
 
-        var maxDailySafetyMultiplier: Decimal = 3
-        var currentBasalSafetyMultiplier: Decimal = 4
-        var useCustomPeakTime: Bool = false
-        var insulinPeakTime: Decimal = 75
-        var skipNeutralTemps: Bool = false
-        var unsuspendIfNoTemp: Bool = false
-        var suspendZerosIOB: Bool = false
-        var min5mCarbimpact: Decimal = 8
-        var autotuneISFAdjustmentFraction: Decimal = 1.0
-        var remainingCarbsFraction: Decimal = 1.0
-        var remainingCarbsCap: Decimal = 90
-        var noisyCGMTargetMultiplier: Decimal = 1.3
+        @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
 
         var insulinActionCurve: Decimal = 6
 
-        var preferences: Preferences {
-            settingsManager.preferences
-        }
-
         var pumpSettings: PumpSettings {
             provider.settings()
         }
@@ -36,18 +32,22 @@ extension AlgorithmAdvancedSettings {
         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
+            subscribePreferencesSetting(\.maxDailySafetyMultiplier, on: $maxDailySafetyMultiplier) {
+                maxDailySafetyMultiplier = $0 }
+            subscribePreferencesSetting(\.currentBasalSafetyMultiplier, on: $currentBasalSafetyMultiplier) {
+                currentBasalSafetyMultiplier = $0 }
+            subscribePreferencesSetting(\.useCustomPeakTime, on: $useCustomPeakTime) { useCustomPeakTime = $0 }
+            subscribePreferencesSetting(\.insulinPeakTime, on: $insulinPeakTime) { insulinPeakTime = $0 }
+            subscribePreferencesSetting(\.unsuspendIfNoTemp, on: $unsuspendIfNoTemp) { unsuspendIfNoTemp = $0 }
+            subscribePreferencesSetting(\.suspendZerosIOB, on: $suspendZerosIOB) { suspendZerosIOB = $0 }
+            subscribePreferencesSetting(\.suspendZerosIOB, on: $suspendZerosIOB) { suspendZerosIOB = $0 }
+            subscribePreferencesSetting(\.min5mCarbimpact, on: $min5mCarbimpact) { min5mCarbimpact = $0 }
+            subscribePreferencesSetting(\.autotuneISFAdjustmentFraction, on: $autotuneISFAdjustmentFraction) {
+                autotuneISFAdjustmentFraction = $0 }
+            subscribePreferencesSetting(\.remainingCarbsFraction, on: $remainingCarbsFraction) { remainingCarbsFraction = $0 }
+            subscribePreferencesSetting(\.remainingCarbsCap, on: $remainingCarbsCap) { remainingCarbsCap = $0 }
+            subscribePreferencesSetting(\.noisyCGMTargetMultiplier, on: $noisyCGMTargetMultiplier) {
+                noisyCGMTargetMultiplier = $0 }
 
             insulinActionCurve = pumpSettings.insulinActionCurve
         }
@@ -56,42 +56,7 @@ extension AlgorithmAdvancedSettings {
             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,

+ 4 - 19
FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension AlgorithmAdvancedSettings {
     struct RootView: BaseView {
         let resolver: Resolver
-        @State var state = StateModel()
+        @StateObject var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?
@@ -14,23 +14,7 @@ extension AlgorithmAdvancedSettings {
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         var body: some View {
             List {
@@ -313,7 +297,8 @@ extension AlgorithmAdvancedSettings {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("Additionals")
             .navigationBarTitleDisplayMode(.automatic)

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

@@ -26,7 +26,6 @@ extension AutosensSettings {
 
         override func subscribe() {
             units = settingsManager.settings.units
-
             autosensMax = settings.preferences.autosensMax
             autosensMin = settings.preferences.autosensMin
             rewindResetsAutosens = settings.preferences.rewindResetsAutosens

+ 3 - 22
FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension AutosensSettings {
     struct RootView: BaseView {
         let resolver: Resolver
-        @State var state = StateModel()
+        @StateObject var state = StateModel()
         @State private var shouldDisplayHint: Bool = false
         @State var hintDetent = PresentationDetent.large
         @State var selectedVerboseHint: String?
@@ -14,23 +14,7 @@ extension AutosensSettings {
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         private var rateFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -178,13 +162,10 @@ extension AutosensSettings {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("Autosens")
             .navigationBarTitleDisplayMode(.automatic)
-            .onDisappear {
-                state.saveIfChanged()
-            }
         }
     }
 }

+ 2 - 17
FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift

@@ -16,22 +16,7 @@ extension AutotuneConfig {
         @State var replaceAlert = false
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         private var isfFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -192,7 +177,7 @@ extension AutotuneConfig {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("Autotune")
             .navigationBarTitleDisplayMode(.automatic)

+ 30 - 34
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -107,16 +107,14 @@ extension BasalProfileEditor {
                 .store(in: &lifetime)
         }
 
-        func validate() {
-            DispatchQueue.main.async {
-                let uniq = Array(Set(self.items))
-                let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
-                sorted.first?.timeIndex = 0
-                if self.items != sorted {
-                    self.items = sorted
-                }
-                self.calcTotal()
+        @MainActor func validate() {
+            let uniq = Array(Set(items))
+            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+            sorted.first?.timeIndex = 0
+            if items != sorted {
+                items = sorted
             }
+            calcTotal()
         }
 
         func availableTimeIndices(_ itemIndex: Int) -> [Int] {
@@ -133,32 +131,30 @@ extension BasalProfileEditor {
             return (0 ..< timeValues.count).filter { !usedIndicesByOtherItems.contains($0) }
         }
 
-        func caluclateChartData() {
-            DispatchQueue.main.async {
-                var basals: [BasalProfile] = []
-                let tzOffset = TimeZone.current.secondsFromGMT() * -1
-
-                basals.append(contentsOf: self.items.enumerated().map { index, item in
-                    let startDate = Date(timeIntervalSinceReferenceDate: self.timeValues[item.timeIndex])
-                    var endDate = Date(timeIntervalSinceReferenceDate: self.timeValues.last!).addingTimeInterval(30 * 60)
-                    if self.items.count > index + 1 {
-                        let nextItem = self.items[index + 1]
-                        endDate = Date(timeIntervalSinceReferenceDate: self.timeValues[nextItem.timeIndex])
-                    }
+        @MainActor func calculateChartData() {
+            var basals: [BasalProfile] = []
+            let tzOffset = TimeZone.current.secondsFromGMT() * -1
 
-                    return BasalProfile(
-                        amount: Double(self.rateValues[item.rateIndex]),
-                        isOverwritten: false,
-                        startDate: startDate.addingTimeInterval(TimeInterval(tzOffset)),
-                        endDate: endDate.addingTimeInterval(TimeInterval(tzOffset))
-                    )
-                })
-                basals.sort(by: {
-                    $0.startDate > $1.startDate
-                })
-
-                self.chartData = basals
-            }
+            basals.append(contentsOf: items.enumerated().map { index, item in
+                let startDate = Date(timeIntervalSinceReferenceDate: self.timeValues[item.timeIndex])
+                var endDate = Date(timeIntervalSinceReferenceDate: self.timeValues.last!).addingTimeInterval(30 * 60)
+                if self.items.count > index + 1 {
+                    let nextItem = self.items[index + 1]
+                    endDate = Date(timeIntervalSinceReferenceDate: self.timeValues[nextItem.timeIndex])
+                }
+
+                return BasalProfile(
+                    amount: Double(self.rateValues[item.rateIndex]),
+                    isOverwritten: false,
+                    startDate: startDate.addingTimeInterval(TimeInterval(tzOffset)),
+                    endDate: endDate.addingTimeInterval(TimeInterval(tzOffset))
+                )
+            })
+            basals.sort(by: {
+                $0.startDate > $1.startDate
+            })
+
+            chartData = basals
         }
     }
 }

+ 21 - 42
FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift

@@ -12,22 +12,7 @@ extension BasalProfileEditor {
             .date(from: DateComponents(year: 2001, month: 01, day: 01, hour: 0, minute: 0, second: 0))
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         private var dateFormatter: DateFormatter {
             let formatter = DateFormatter()
@@ -123,6 +108,16 @@ extension BasalProfileEditor {
 
         var body: some View {
             Form {
+                if !state.canAdd {
+                    Section {
+                        VStack(alignment: .leading) {
+                            Text(
+                                "Basal profile covers 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
+                            ).bold()
+                        }
+                    }.listRowBackground(Color.tabBar)
+                }
+
                 Section(header: Text("Schedule")) {
                     if !state.items.isEmpty {
                         basalScheduleChart.padding(.vertical)
@@ -154,24 +149,26 @@ extension BasalProfileEditor {
             }
             .onChange(of: state.items) {
                 state.calcTotal()
-                state.caluclateChartData()
+                state.calculateChartData()
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .navigationTitle("Basal Profile")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    EditButton()
+                if state.items.isNotEmpty {
+                    ToolbarItem(placement: .topBarTrailing) {
+                        EditButton()
+                    }
                 }
                 ToolbarItem(placement: .topBarTrailing) {
-                    addButton
+                    Button(action: { state.add() }) { Image(systemName: "plus") }.disabled(!state.canAdd)
                 }
             })
             .environment(\.editMode, $editMode)
             .onAppear {
                 configureView()
                 state.validate()
-                state.caluclateChartData()
+                state.calculateChartData()
             }
         }
 
@@ -207,7 +204,7 @@ extension BasalProfileEditor {
                 }.listRowBackground(Color.chart)
             }
             .padding(.top)
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .navigationTitle("Set Rate")
             .navigationBarTitleDisplayMode(.automatic)
         }
@@ -234,28 +231,10 @@ extension BasalProfileEditor {
             }
         }
 
-        private var addButton: some View {
-            guard state.canAdd else {
-                return AnyView(EmptyView())
-            }
-
-            switch editMode {
-            case .inactive:
-                return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
-            default:
-                return AnyView(EmptyView())
-            }
-        }
-
-        func onAdd() {
-            state.add()
-        }
-
         private func onDelete(offsets: IndexSet) {
             state.items.remove(atOffsets: offsets)
             state.validate()
-            state.calcTotal()
-            state.caluclateChartData()
+            state.calculateChartData()
         }
     }
 }

+ 15 - 0
FreeAPS/Sources/Modules/Base/BaseStateModel.swift

@@ -58,4 +58,19 @@ class BaseStateModel<Provider>: StateModel, Injectable where Provider: FreeAPS.P
             }
             .store(in: &lifetime)
     }
+
+    func subscribePreferencesSetting<T: Equatable, U: Publisher>(
+        _ keyPath: WritableKeyPath<Preferences, T>,
+        on preferencesPublisher: U, initial: (T) -> Void, map: ((T) -> (T))? = nil, didSet: ((T) -> Void)? = nil
+    ) where U.Output == T, U.Failure == Never {
+        initial(settingsManager.preferences[keyPath: keyPath])
+        preferencesPublisher
+            .removeDuplicates()
+            .map(map ?? { $0 })
+            .sink { [weak self] value in
+                self?.settingsManager.preferences[keyPath: keyPath] = value
+                didSet?(value)
+            }
+            .store(in: &lifetime)
+    }
 }

+ 2 - 17
FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -15,22 +15,7 @@ extension BolusCalculatorConfig {
         @State private var booleanPlaceholder: Bool = false
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         private var conversionFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -132,7 +117,7 @@ extension BolusCalculatorConfig {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationBarTitle("Bolus Calculator")
             .navigationBarTitleDisplayMode(.automatic)

+ 28 - 42
FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift

@@ -17,22 +17,7 @@ extension CGM {
         @State private var booleanPlaceholder: Bool = false
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         var body: some View {
             NavigationView {
@@ -210,10 +195,11 @@ extension CGM {
                         verboseHint: "Smooth Glucose Value… bla bla bla"
                     )
                 }
-                .scrollContentBackground(.hidden).background(color)
+                .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
                 .onAppear(perform: configureView)
                 .navigationTitle("CGM")
                 .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: displayClose ? Button("Close", action: state.hideModal) : nil)
                 .sheet(isPresented: $shouldDisplayHint) {
                     SettingInputHintView(
                         hintDetent: $hintDetent,
@@ -223,37 +209,37 @@ extension CGM {
                         sheetTitle: "Help"
                     )
                 }
-                .sheet(isPresented: $setupCGM) {
-                    if let cgmFetchManager = state.cgmManager,
-                       let cgmManager = cgmFetchManager.cgmManager,
-                       state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
-                       state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
-                    {
-                        CGMSettingsView(
-                            cgmManager: cgmManager,
-                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                            unit: state.settingsManager.settings.units,
-                            completionDelegate: state
-                        )
-                    } else {
-                        CGMSetupView(
-                            CGMType: state.cgmCurrent,
-                            bluetoothManager: state.provider.apsManager.bluetoothManager!,
-                            unit: state.settingsManager.settings.units,
-                            completionDelegate: state,
-                            setupDelegate: state,
-                            pluginCGMManager: self.state.pluginCGMManager
-                        )
-                    }
-                }
-                .onChange(of: setupCGM) { setupCGM in
+                .onChange(of: setupCGM) { _, setupCGM in
                     state.setupCGM = setupCGM
                 }
-                .onChange(of: state.setupCGM) { setupCGM in
+                .onChange(of: state.setupCGM) { _, setupCGM in
                     self.setupCGM = setupCGM
                 }
                 .screenNavigation(self)
             }
+            .sheet(isPresented: $setupCGM) {
+                if let cgmFetchManager = state.cgmManager,
+                   let cgmManager = cgmFetchManager.cgmManager,
+                   state.cgmCurrent.type == cgmFetchManager.cgmGlucoseSourceType,
+                   state.cgmCurrent.id == cgmFetchManager.cgmGlucosePluginId
+                {
+                    CGMSettingsView(
+                        cgmManager: cgmManager,
+                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                        unit: state.settingsManager.settings.units,
+                        completionDelegate: state
+                    )
+                } else {
+                    CGMSetupView(
+                        CGMType: state.cgmCurrent,
+                        bluetoothManager: state.provider.apsManager.bluetoothManager!,
+                        unit: state.settingsManager.settings.units,
+                        completionDelegate: state,
+                        setupDelegate: state,
+                        pluginCGMManager: self.state.pluginCGMManager
+                    )
+                }
+            }
         }
     }
 }

+ 2 - 18
FreeAPS/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift

@@ -14,23 +14,7 @@ extension CalendarEventSettings {
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         var body: some View {
             List {
@@ -123,7 +107,7 @@ extension CalendarEventSettings {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("Calendar Events")
             .navigationBarTitleDisplayMode(.automatic)

+ 2 - 17
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -7,22 +7,7 @@ extension Calibrations {
         @State var state = StateModel()
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         private var formatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -107,7 +92,7 @@ extension Calibrations {
                     }
                 }
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .dynamicTypeSize(...DynamicTypeSize.xxLarge)
             .onAppear(perform: configureView)
             .navigationTitle("Calibrations")

+ 18 - 38
FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift

@@ -9,22 +9,7 @@ extension CarbRatioEditor {
         @State private var editMode = EditMode.inactive
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         private var dateFormatter: DateFormatter {
             let formatter = DateFormatter()
@@ -94,21 +79,33 @@ extension CarbRatioEditor {
                     }.listRowBackground(Color.chart)
                 }
 
+                if !state.canAdd {
+                    Section {
+                        VStack(alignment: .leading) {
+                            Text(
+                                "Carb Ratios cover 24 hours. You cannot add more rates. Please remove or adjust existing rates to make space."
+                            ).bold()
+                        }
+                    }.listRowBackground(Color.tabBar)
+                }
+
                 Section(header: Text("Schedule")) {
                     list
                 }.listRowBackground(Color.chart)
             }
             .safeAreaInset(edge: .bottom, spacing: 30) { saveButton }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("Carb Ratios")
             .navigationBarTitleDisplayMode(.automatic)
             .toolbar(content: {
-                ToolbarItem(placement: .topBarTrailing) {
-                    EditButton()
+                if state.items.isNotEmpty {
+                    ToolbarItem(placement: .topBarTrailing) {
+                        EditButton()
+                    }
                 }
                 ToolbarItem(placement: .topBarTrailing) {
-                    addButton
+                    Button(action: { state.add() }) { Image(systemName: "plus") }.disabled(!state.canAdd)
                 }
             })
             .environment(\.editMode, $editMode)
@@ -148,7 +145,7 @@ extension CarbRatioEditor {
                 }.listRowBackground(Color.chart)
             }
             .padding(.top)
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .navigationTitle("Set Ratio")
             .navigationBarTitleDisplayMode(.automatic)
         }
@@ -234,23 +231,6 @@ extension CarbRatioEditor {
             }
         }
 
-        private var addButton: some View {
-            guard state.canAdd else {
-                return AnyView(EmptyView())
-            }
-
-            switch editMode {
-            case .inactive:
-                return AnyView(Button(action: onAdd) { Image(systemName: "plus") })
-            default:
-                return AnyView(EmptyView())
-            }
-        }
-
-        func onAdd() {
-            state.add()
-        }
-
         private func onDelete(offsets: IndexSet) {
             state.items.remove(atOffsets: offsets)
             state.validate()

+ 2 - 17
FreeAPS/Sources/Modules/ConfigEditor/View/ConfigEditorRootView.swift

@@ -9,22 +9,7 @@ extension ConfigEditor {
         @State private var showShareSheet = false
 
         @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
-                )
-        }
+        @Environment(AppState.self) var appState
 
         var body: some View {
             ZStack {
@@ -58,7 +43,7 @@ extension ConfigEditor {
                     .navigationBarTitleDisplayMode(.automatic)
                     .padding()
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
         }
     }
 }

+ 8 - 0
FreeAPS/Sources/Modules/ContactTrick/ContactTrickDataFlow.swift

@@ -0,0 +1,8 @@
+import Combine
+import Foundation
+
+enum ContactTrick {
+    enum Config {}
+}
+
+protocol ContactTrickProvider: Provider {}

+ 6 - 0
FreeAPS/Sources/Modules/ContactTrick/ContactTrickProvider.swift

@@ -0,0 +1,6 @@
+import Combine
+import Foundation
+
+extension ContactTrick {
+    final class Provider: BaseProvider, ContactTrickProvider {}
+}

+ 179 - 0
FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift

@@ -0,0 +1,179 @@
+import ConnectIQ
+import CoreData
+import SwiftUI
+
+extension ContactTrick {
+    @Observable final class StateModel: BaseStateModel<Provider>, ContactTrickManagerDelegate {
+        @ObservationIgnored @Injected() var contactTrickStorage: ContactTrickStorage!
+        @ObservationIgnored @Injected() var contactTrickManager: ContactTrickManager!
+
+        var contactTrickEntries = [ContactTrickEntry]()
+        var units: GlucoseUnits = .mmolL
+        // Help Sheet
+        var isHelpSheetPresented: Bool = false
+        var helpSheetDetent = PresentationDetent.large
+
+        // Current state for live preview
+        var state = ContactTrickState()
+
+        /// Subscribes to updates and initializes data fetching.
+        override func subscribe() {
+            units = settingsManager.settings.units
+            contactTrickManager.delegate = self
+
+            Task {
+                /// Initial fetch to fill the ContactTrickEntry array
+                await fetchContactTrickEntriesAndUpdateUI()
+
+                // Initial state update is needed for preview
+                await contactTrickManager.updateContactTrickState()
+            }
+        }
+
+        func contactTrickManagerDidUpdateState(_ state: ContactTrickState) {
+            Task { @MainActor in
+                self.state = state
+            }
+        }
+
+        /// Fetches all ContactTrickEntries and validates them against iOS Contacts.
+        func fetchContactTrickEntriesAndUpdateUI() async {
+            // 1. Get all entries from Core Data
+            let cdEntries = await contactTrickStorage.fetchContactTrickEntries()
+
+            // 2. Validate entries against iOS Contacts
+            let validatedEntries = await validateEntries(cdEntries)
+
+            // 3. Update UI with validated entries
+            await MainActor.run {
+                self.contactTrickEntries = validatedEntries
+            }
+        }
+
+        /// Validates entries against iOS Contacts and removes invalid ones
+        private func validateEntries(_ entries: [ContactTrickEntry]) async -> [ContactTrickEntry] {
+            var validated: [ContactTrickEntry] = []
+
+            for entry in entries {
+                if let contactId = entry.contactId {
+                    // Check if contact still exists in iOS Contacts
+                    let exists = await contactTrickManager.validateContactExists(withIdentifier: contactId)
+
+                    if exists {
+                        validated.append(entry)
+                    } else {
+                        // Contact was deleted in iOS, remove from Core Data
+                        if let objectID = entry.managedObjectID {
+                            await contactTrickStorage.deleteContactTrickEntry(objectID)
+                            debugPrint("Removed orphaned contact entry: \(entry.name)")
+                        }
+                    }
+                }
+            }
+
+            return validated
+        }
+
+        /// Creates a new contact in Apple Contacts and saves it to Core Data.
+        /// - Parameters:
+        ///   - entry: The ContactTrickEntry to be saved.
+        ///   - name: The name of the contact.
+        func createAndSaveContactTrick(entry: ContactTrickEntry, name: String) async {
+            // 1. Check for contact access permissions.
+            let hasAccess = await contactTrickManager.requestAccess()
+            guard hasAccess else {
+                debugPrint("\(DebuggingIdentifiers.failed) No access to contacts.")
+                return
+            }
+
+            // 2. Create the contact and retrieve its `identifier`.
+            guard let contactId = await contactTrickManager.createContact(name: name) else {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to create contact.")
+                return
+            }
+
+            // 3. Update the entry with the `contactId`.
+            var updatedEntry = entry
+            updatedEntry.contactId = contactId
+            updatedEntry.name = name
+
+            // 4. Save the contact to Core Data.
+            await addContactTrickEntry(updatedEntry)
+
+            // 5. Update ContactTrickState and set the image for the newly created contact
+            await contactTrickManager.updateContactTrickState()
+            await contactTrickManager.setImageForContact(contactId: contactId)
+        }
+
+        /// Adds a ContactTrickEntry to Core Data.
+        /// - Parameter entry: The ContactTrickEntry to be saved.
+        func addContactTrickEntry(_ entry: ContactTrickEntry) async {
+            await contactTrickStorage.storeContactTrickEntry(entry)
+            await fetchContactTrickEntriesAndUpdateUI()
+        }
+
+        /// Deletes a contact from Apple Contacts and Core Data.
+        /// - Parameter entry: The ContactTrickEntry representing the contact to be deleted.
+        func deleteContact(entry: ContactTrickEntry) async {
+            guard let contactId = entry.contactId else {
+                debugPrint("\(DebuggingIdentifiers.failed) Contact does not have a valid ID.")
+                return
+            }
+
+            // 1. Attempt to delete the contact from Apple Contacts.
+            let contactDeleted = await contactTrickManager.deleteContact(withIdentifier: contactId)
+            if contactDeleted {
+                debugPrint("\(DebuggingIdentifiers.succeeded) Contact successfully deleted from Apple Contacts: \(contactId)")
+            } else {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to delete contact from Apple Contacts. Check if it exists.")
+            }
+
+            // 2. Delete the entry from Core Data.
+            if let objectID = entry.managedObjectID {
+                await deleteContactTrick(objectID: objectID)
+            }
+        }
+
+        /// Deletes a Core Data entry.
+        /// - Parameter objectID: The Managed Object ID of the entry to be deleted.
+        func deleteContactTrick(objectID: NSManagedObjectID) async {
+            await contactTrickStorage.deleteContactTrickEntry(objectID)
+            await fetchContactTrickEntriesAndUpdateUI()
+        }
+
+        /// Updates a contact in Apple Contacts and Core Data.
+        /// - Parameters:
+        ///   - entry: The ContactTrickEntry to be updated.
+        func updateContact(with entry: ContactTrickEntry) async {
+            guard let contactId = entry.contactId else {
+                debugPrint("\(DebuggingIdentifiers.failed) Contact does not have a valid ID.")
+                return
+            }
+
+            // 1. Update the entry in Core Data.
+            await updateContactTrick(entry)
+
+            // 2. Update the contact in Apple Contacts.
+
+            /// Update name
+            let contactUpdated = await contactTrickManager
+                .updateContact(withIdentifier: contactId, newName: entry.name) // TODO: - Probably not needed anymore
+
+            guard contactUpdated else {
+                debugPrint("\(DebuggingIdentifiers.failed) Failed to update contact.")
+                return
+            }
+
+            /// Update state and image
+            await contactTrickManager.updateContactTrickState()
+            await contactTrickManager.setImageForContact(contactId: contactId)
+        }
+
+        /// Updates a Core Data entry.
+        /// - Parameter entry: The updated ContactTrickEntry.
+        func updateContactTrick(_ entry: ContactTrickEntry) async {
+            await contactTrickStorage.updateContactTrickEntry(entry)
+            await fetchContactTrickEntriesAndUpdateUI()
+        }
+    }
+}

+ 248 - 0
FreeAPS/Sources/Modules/ContactTrick/View/AddContactTrickSheet.swift

@@ -0,0 +1,248 @@
+import SwiftUI
+
+struct AddContactTrickSheet: View {
+    @Environment(\.dismiss) var dismiss
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    @ObservedObject var state: ContactTrick.StateModel
+
+    @State private var hasHighContrast: Bool = true
+    @State private var ringWidth: ContactTrickEntry.RingWidth = .regular
+    @State private var ringGap: ContactTrickEntry.RingGap = .small
+    @State private var layout: ContactTrickLayout = .single
+    @State private var primary: ContactTrickValue = .glucose
+    @State private var top: ContactTrickValue = .none
+    @State private var bottom: ContactTrickValue = .trend
+    @State private var ring: ContactTrickLargeRing = .none
+    @State private var fontSize: ContactTrickEntry.FontSize = .regular
+    @State private var secondaryFontSize: ContactTrickEntry.FontSize = .small
+    @State private var fontWeight: Font.Weight = .medium
+    @State private var fontWidth: Font.Width = .standard
+
+    private var previewEntry: ContactTrickEntry {
+        ContactTrickEntry(
+            id: UUID(),
+            name: "", // automatically set and populated
+            layout: layout,
+            ring: ring,
+            primary: primary,
+            top: top,
+            bottom: bottom,
+            contactId: nil, // not needed for preview, gets set later in ContactTrickStateModel via ContactTrickManager
+            hasHighContrast: hasHighContrast,
+            ringWidth: ringWidth,
+            ringGap: ringGap,
+            fontSize: fontSize,
+            secondaryFontSize: secondaryFontSize,
+            fontWeight: fontWeight,
+            fontWidth: fontWidth
+        )
+    }
+
+    var body: some View {
+        NavigationView {
+            VStack {
+                // Preview Section
+                HStack {
+                    Spacer()
+                    ZStack {
+                        Circle()
+                            .fill(previewEntry.hasHighContrast ? .black : .white)
+                            .foregroundColor(.white)
+                            .frame(width: 100, height: 100)
+                        Image(uiImage: ContactPicture.getImage(contact: previewEntry, state: state.state))
+                            .resizable()
+                            .frame(width: 100, height: 100)
+                            .clipShape(Circle())
+                        Circle()
+                            .stroke(lineWidth: 2)
+                            .foregroundColor(.white)
+                            .frame(width: 100, height: 100)
+                    }
+                    Spacer()
+                }
+                .padding(.top, 40)
+                .padding(.bottom)
+
+                Form {
+                    // Layout Section
+                    Section(header: Text("Style")) {
+                        Picker("Layout", selection: $layout) {
+                            ForEach(ContactTrickLayout.allCases, id: \.id) { layout in
+                                Text(layout.displayName).tag(layout)
+                            }
+                        }
+                        Toggle("High Contrast Mode", isOn: $hasHighContrast)
+                    }.listRowBackground(Color.chart)
+
+                    // Primary Value Section
+                    Section(header: Text("Display Values")) {
+                        if layout == .single {
+                            Picker("Primary", selection: $primary) {
+                                ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                    Text(value.displayName).tag(value)
+                                }
+                            }
+                        }
+
+                        Picker("Top Value", selection: $top) {
+                            ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                Text(value.displayName).tag(value)
+                            }
+                        }
+                        Picker("Bottom Value", selection: $bottom) {
+                            ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                Text(value.displayName).tag(value)
+                            }
+                        }
+
+                    }.listRowBackground(Color.chart)
+
+                    // Ring Settings Section
+                    Section(header: Text("Ring Settings")) {
+                        Picker("Ring Type", selection: $ring) {
+                            ForEach(ContactTrickLargeRing.allCases, id: \.self) { ring in
+                                Text(ring.displayName).tag(ring)
+                            }
+                        }
+
+                        if ring != .none {
+                            Picker("Ring Width", selection: $ringWidth) {
+                                ForEach(ContactTrickEntry.RingWidth.allCases, id: \.self) { width in
+                                    Text(width.displayName).tag(width)
+                                }
+                            }
+                            Picker("Ring Gap", selection: $ringGap) {
+                                ForEach(ContactTrickEntry.RingGap.allCases, id: \.self) { gap in
+                                    Text(gap.displayName).tag(gap)
+                                }
+                            }
+                        }
+                    }.listRowBackground(Color.chart)
+
+                    // Font Settings Section
+                    Section(header: Text("Font Settings")) {
+                        fontSizePicker
+                        secondaryFontSizePicker
+                        fontWeightPicker
+                        fontWidthPicker
+                    }.listRowBackground(Color.chart)
+                }
+
+                stickySaveButton
+            }
+            .navigationTitle("Add Contact Items")
+            .navigationBarTitleDisplayMode(.inline)
+            .listSectionSpacing(10)
+            .padding(.top, 30)
+            .ignoresSafeArea(edges: .top)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button("Cancel") {
+                        dismiss()
+                    }
+                }
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(
+                        action: {
+                            state.isHelpSheetPresented.toggle()
+                        },
+                        label: {
+                            Image(systemName: "questionmark.circle")
+                        }
+                    )
+                }
+            }
+            .sheet(isPresented: $state.isHelpSheetPresented) {
+                NavigationStack {
+                    List {
+                        Text("Lorem Ipsum Dolor Sit Amet")
+                    }
+                    .padding(.trailing, 10)
+                    .navigationBarTitle("Help", displayMode: .inline)
+
+                    Button { state.isHelpSheetPresented.toggle() }
+                    label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                        .buttonStyle(.bordered)
+                        .padding(.top)
+                }
+                .padding()
+                .presentationDetents(
+                    [.fraction(0.9), .large],
+                    selection: $state.helpSheetDetent
+                )
+            }
+        }
+    }
+
+    var stickySaveButton: some View {
+        ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                saveNewEntry()
+            }, label: {
+                Text("Save").padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                .background(Color(.systemBlue))
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding(5)
+        }
+    }
+
+    private var fontSizePicker: some View {
+        Picker("Font Size", selection: $fontSize) {
+            ForEach(ContactTrickEntry.FontSize.allCases, id: \.self) { size in
+                Text(size.displayName).tag(size)
+            }
+        }
+    }
+
+    private var secondaryFontSizePicker: some View {
+        Picker("Secondary Font Size", selection: $secondaryFontSize) {
+            ForEach(ContactTrickEntry.FontSize.allCases, id: \.self) { size in
+                Text(size.displayName).tag(size)
+            }
+        }
+    }
+
+    private var fontWeightPicker: some View {
+        Picker("Font Weight", selection: $fontWeight) {
+            ForEach(
+                [Font.Weight.light, Font.Weight.regular, Font.Weight.medium, Font.Weight.bold, Font.Weight.black],
+                id: \.self
+            ) { weight in
+                Text("\(weight.displayName)".capitalized).tag(weight)
+            }
+        }
+    }
+
+    private var fontWidthPicker: some View {
+        Picker("Font Width", selection: $fontWidth) {
+            ForEach(
+                [Font.Width.standard, Font.Width.condensed, Font.Width.expanded],
+                id: \.self
+            ) { width in
+                Text("\(width.displayName)".capitalized).tag(width)
+            }
+        }
+    }
+
+    private func saveNewEntry() {
+        // Save the currently previewed entry
+        Task {
+            await state.createAndSaveContactTrick(entry: previewEntry, name: "Trio \(state.contactTrickEntries.count + 1)")
+            dismiss()
+        }
+    }
+}

+ 216 - 0
FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickDetailView.swift

@@ -0,0 +1,216 @@
+import SwiftUI
+
+struct ContactTrickDetailView: View {
+    @Environment(\.dismiss) var dismiss
+    @Environment(\.colorScheme) var colorScheme
+    @Environment(AppState.self) var appState
+
+    @ObservedObject var state: ContactTrick.StateModel
+
+    @State private var contactTrickEntry: ContactTrickEntry
+    @State private var initialContactTrickEntry: ContactTrickEntry
+
+    init(entry: ContactTrickEntry, state: ContactTrick.StateModel) {
+        self.state = state
+        _contactTrickEntry = State(initialValue: entry)
+        _initialContactTrickEntry = State(initialValue: entry)
+    }
+
+    var body: some View {
+        VStack {
+            HStack {
+                // TODO: - make this beautiful @Dan
+                Spacer()
+                ZStack {
+                    Circle()
+                        .fill(contactTrickEntry.hasHighContrast ? .black : .white)
+                        .foregroundColor(.white)
+                        .frame(width: 100, height: 100)
+                    Image(uiImage: ContactPicture.getImage(contact: contactTrickEntry, state: state.state))
+                        .resizable()
+                        .frame(width: 100, height: 100)
+                        .clipShape(Circle())
+                    Circle()
+                        .stroke(lineWidth: 2)
+                        .foregroundColor(.white)
+                        .frame(width: 100, height: 100)
+                }
+                Spacer()
+            }
+            .padding(.top, 80)
+            .padding(.bottom)
+
+            Form {
+                Section(header: Text("Style")) {
+                    Picker("Layout", selection: $contactTrickEntry.layout) {
+                        ForEach(ContactTrickLayout.allCases, id: \.id) { layout in
+                            Text(layout.displayName).tag(layout)
+                        }
+                    }
+                    Toggle("High Contrast Mode", isOn: $contactTrickEntry.hasHighContrast)
+                }.listRowBackground(Color.chart)
+
+                Section(header: Text("Display Values")) {
+                    if contactTrickEntry.layout == .single {
+                        Picker("Primary", selection: $contactTrickEntry.primary) {
+                            ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                                Text(value.displayName).tag(value)
+                            }
+                        }
+                    }
+
+                    Picker("Top Value", selection: $contactTrickEntry.top) {
+                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                            Text(value.displayName).tag(value)
+                        }
+                    }
+
+                    Picker("Bottom Value", selection: $contactTrickEntry.bottom) {
+                        ForEach(ContactTrickValue.allCases, id: \.id) { value in
+                            Text(value.displayName).tag(value)
+                        }
+                    }
+                }.listRowBackground(Color.chart)
+
+                // Ring Settings Section
+                Section(header: Text("Ring Settings")) {
+                    Picker("Ring Type", selection: $contactTrickEntry.ring) {
+                        ForEach(ContactTrickLargeRing.allCases, id: \.self) { ring in
+                            Text(ring.displayName).tag(ring)
+                        }
+                    }
+
+                    if contactTrickEntry.ring != .none {
+                        Picker("Ring Width", selection: $contactTrickEntry.ringWidth) {
+                            ForEach(ContactTrickEntry.RingWidth.allCases, id: \.self) { width in
+                                Text(width.displayName).tag(width)
+                            }
+                        }
+                        Picker("Ring Gap", selection: $contactTrickEntry.ringGap) {
+                            ForEach(ContactTrickEntry.RingGap.allCases, id: \.self) { gap in
+                                Text(gap.displayName).tag(gap)
+                            }
+                        }
+                    }
+                }.listRowBackground(Color.chart)
+
+                // Font Settings Section
+                Section(header: Text("Font Settings")) {
+                    fontSizePicker
+                    secondaryFontSizePicker
+                    fontWeightPicker
+                    fontWidthPicker
+                }.listRowBackground(Color.chart)
+            }
+        }
+        .navigationTitle("Edit Contact Items")
+        .navigationBarTitleDisplayMode(.inline)
+        .safeAreaInset(edge: .bottom, spacing: 0) { stickySaveButton }
+        .listSectionSpacing(10)
+        .padding(.top, 30)
+        .ignoresSafeArea(edges: .top)
+        .scrollContentBackground(.hidden)
+        .background(appState.trioBackgroundColor(for: colorScheme))
+        .toolbar {
+            ToolbarItem(placement: .topBarTrailing) {
+                Button(
+                    action: {
+                        state.isHelpSheetPresented.toggle()
+                    },
+                    label: {
+                        Image(systemName: "questionmark.circle")
+                    }
+                )
+            }
+        }
+        .sheet(isPresented: $state.isHelpSheetPresented) {
+            NavigationStack {
+                List {
+                    Text("Lorem Ipsum Dolor Sit Amet")
+                }
+                .padding(.trailing, 10)
+                .navigationBarTitle("Help", displayMode: .inline)
+
+                Button { state.isHelpSheetPresented.toggle() }
+                label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                    .buttonStyle(.bordered)
+                    .padding(.top)
+            }
+            .padding()
+            .presentationDetents(
+                [.fraction(0.9), .large],
+                selection: $state.helpSheetDetent
+            )
+        }
+    }
+
+    private func saveChanges() {
+        Task {
+            await state.updateContact(with: contactTrickEntry)
+            dismiss()
+        }
+    }
+
+    var stickySaveButton: some View {
+        var isUnchanged: Bool { initialContactTrickEntry == contactTrickEntry }
+
+        return ZStack {
+            Rectangle()
+                .frame(width: UIScreen.main.bounds.width, height: 65)
+                .foregroundStyle(colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white)
+                .background(.thinMaterial)
+                .opacity(0.8)
+                .clipShape(Rectangle())
+
+            Button(action: {
+                saveChanges()
+            }, label: {
+                Text("Save").padding(10)
+            })
+                .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
+                .background(isUnchanged ? Color(.systemGray4) : Color(.systemBlue))
+                .disabled(isUnchanged)
+                .tint(.white)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding(5)
+        }
+    }
+
+    private var fontSizePicker: some View {
+        Picker("Font Size", selection: $contactTrickEntry.fontSize) {
+            ForEach(ContactTrickEntry.FontSize.allCases, id: \.self) { size in
+                Text(size.displayName).tag(size)
+            }
+        }
+    }
+
+    private var secondaryFontSizePicker: some View {
+        Picker("Secondary Font Size", selection: $contactTrickEntry.secondaryFontSize) {
+            ForEach(ContactTrickEntry.FontSize.allCases, id: \.self) { size in
+                Text(size.displayName).tag(size)
+            }
+        }
+    }
+
+    private var fontWeightPicker: some View {
+        Picker("Font Weight", selection: $contactTrickEntry.fontWeight) {
+            ForEach(
+                [Font.Weight.light, Font.Weight.regular, Font.Weight.medium, Font.Weight.bold, Font.Weight.black],
+                id: \.self
+            ) { weight in
+                Text("\(weight.displayName)".capitalized).tag(weight)
+            }
+        }
+    }
+
+    private var fontWidthPicker: some View {
+        Picker("Font Width", selection: $contactTrickEntry.fontWidth) {
+            ForEach(
+                [Font.Width.standard, Font.Width.condensed, Font.Width.expanded],
+                id: \.self
+            ) { width in
+                Text("\(width.displayName)".capitalized).tag(width)
+            }
+        }
+    }
+}

+ 89 - 0
FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift

@@ -0,0 +1,89 @@
+import Contacts
+import ContactsUI
+import SwiftUI
+import Swinject
+
+extension ContactTrick {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @State var state = StateModel()
+        @State private var isAddSheetPresented: Bool = false
+
+        @Environment(\.colorScheme) var colorScheme
+        @Environment(AppState.self) var appState
+
+        var body: some View {
+            Form {
+                contactTrickList
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Contacts Configuration")
+            .navigationBarTitleDisplayMode(.large)
+            .scrollContentBackground(.hidden)
+            .background(appState.trioBackgroundColor(for: colorScheme))
+            .toolbar {
+                ToolbarItem(placement: .topBarTrailing) {
+                    Button(action: {
+                        isAddSheetPresented.toggle()
+                    }) {
+                        HStack {
+                            Text("Add Contact")
+                            Image(systemName: "plus")
+                        }
+                    }
+                }
+            }
+            .sheet(isPresented: $isAddSheetPresented) {
+                AddContactTrickSheet(state: state)
+            }
+        }
+
+        private var contactTrickList: some View {
+            List {
+                if state.contactTrickEntries.isEmpty {
+                    Section(
+                        header: Text(""),
+                        content: {
+                            Text("No Contact Trick Entries.")
+                        }
+                    ).listRowBackground(Color.chart)
+                } else {
+                    ForEach(state.contactTrickEntries, id: \.id) { entry in
+                        NavigationLink(destination: ContactTrickDetailView(entry: entry, state: state)) {
+                            HStack {
+                                ZStack {
+                                    Circle()
+                                        .fill(entry.hasHighContrast ? .black : .white)
+                                        .foregroundColor(.white)
+                                        .frame(width: 40, height: 40)
+
+                                    Image(uiImage: ContactPicture.getImage(contact: entry, state: state.state))
+                                        .resizable()
+                                        .frame(width: 40, height: 40)
+                                        .clipShape(Circle())
+
+                                    Circle()
+                                        .stroke(lineWidth: 2)
+                                        .foregroundColor(.white)
+                                        .frame(width: 40, height: 40)
+                                }
+
+                                Text("\(entry.name)")
+                            }
+                        }
+                    }
+                    .onDelete(perform: onDelete)
+                }
+            }.listRowBackground(Color.chart)
+        }
+
+        private func onDelete(offsets: IndexSet) {
+            Task {
+                for offset in offsets {
+                    let entry = state.contactTrickEntries[offset]
+                    await state.deleteContact(entry: entry)
+                }
+            }
+        }
+    }
+}

+ 3 - 0
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -10,6 +10,7 @@ enum DataTable {
         case treatments
         case meals
         case glucose
+        case adjustments
 
         var id: String { rawValue }
 
@@ -22,6 +23,8 @@ enum DataTable {
                 name = "Meals"
             case .glucose:
                 name = "Glucose"
+            case .adjustments:
+                name = "Adjustments"
             }
 
             return NSLocalizedString(name, comment: "History Mode")

+ 16 - 29
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -21,7 +21,6 @@ extension DataTable {
         var glucose: [Glucose] = []
         var meals: [Treatment] = []
         var manualGlucose: Decimal = 0
-        var maxBolus: Decimal = 0
         var waitForSuggestion: Bool = false
 
         var insulinEntryDeleted: Bool = false
@@ -31,8 +30,8 @@ extension DataTable {
 
         override func subscribe() {
             units = settingsManager.settings.units
-            maxBolus = provider.pumpSettings().maxBolus
             broadcaster.register(DeterminationObserver.self, observer: self)
+            broadcaster.register(SettingsObserver.self, observer: self)
         }
 
         func isGlucoseDataFresh(_ glucoseDate: Date?) -> Bool {
@@ -90,6 +89,14 @@ extension DataTable {
             }
         }
 
+        func addManualGlucose() {
+            // Always save value in mg/dL
+            let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
+            let glucoseAsInt = Int(glucose)
+
+            glucoseStorage.addManualGlucose(glucose: glucoseAsInt)
+        }
+
         // Carb and FPU deletion from history
         /// - **Parameter**: NSManagedObjectID to be able to transfer the object safely from one thread to another thread
         func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID) {
@@ -107,7 +114,7 @@ extension DataTable {
             // Delete from Apple Health/Tidepool
             await deleteCarbsFromServices(treatmentObjectID)
 
-            // Delete from Core Data
+            // Delete carbs from Core Data
             await carbsStorage.deleteCarbs(treatmentObjectID)
 
             // Perform a determine basal sync to update cob
@@ -159,7 +166,7 @@ extension DataTable {
                                 withSyncId: id,
                                 carbs: Decimal(carbEntry.carbs),
                                 at: entryDate,
-                                enteredBy: CarbsEntry.manual
+                                enteredBy: CarbsEntry.local
                             )
                         }
                     }
@@ -239,37 +246,17 @@ extension DataTable {
                 }
             }
         }
-
-        func addManualGlucose() {
-            let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
-            let glucoseAsInt = Int(glucose)
-
-            // save to core data
-            coredataContext.perform {
-                let newItem = GlucoseStored(context: self.coredataContext)
-                newItem.id = UUID()
-                newItem.date = Date()
-                newItem.glucose = Int16(glucoseAsInt)
-                newItem.isManual = true
-                newItem.isUploadedToNS = false
-                newItem.isUploadedToHealth = false
-                newItem.isUploadedToTidepool = false
-
-                do {
-                    guard self.coredataContext.hasChanges else { return }
-                    try self.coredataContext.save()
-                } catch {
-                    print(error.localizedDescription)
-                }
-            }
-        }
     }
 }
 
-extension DataTable.StateModel: DeterminationObserver {
+extension DataTable.StateModel: DeterminationObserver, SettingsObserver {
     func determinationDidUpdate(_: Determination) {
         DispatchQueue.main.async {
             self.waitForSuggestion = false
         }
     }
+
+    func settingsDidChange(_: FreeAPSSettings) {
+        units = settingsManager.settings.units
+    }
 }

+ 162 - 67
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -21,6 +21,7 @@ extension DataTable {
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.managedObjectContext) var context
+        @Environment(AppState.self) var appState
 
         @FetchRequest(
             entity: GlucoseStored.entity(),
@@ -43,26 +44,19 @@ extension DataTable {
             animation: .bouncy
         ) var carbEntryStored: FetchedResults<CarbEntryStored>
 
-        private var insulinFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 2
-            return formatter
-        }
-
-        private var glucoseFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
+        @FetchRequest(
+            entity: OverrideRunStored.entity(),
+            sortDescriptors: [NSSortDescriptor(keyPath: \OverrideRunStored.startDate, ascending: false)],
+            predicate: NSPredicate.overridesRunStoredFromOneDayAgo,
+            animation: .bouncy
+        ) var overrideRunStored: FetchedResults<OverrideRunStored>
 
-            if state.units == .mmolL {
-                formatter.maximumFractionDigits = 1
-                formatter.minimumFractionDigits = 1
-                formatter.roundingMode = .halfUp
-            } else {
-                formatter.maximumFractionDigits = 0
-            }
-            return formatter
-        }
+        @FetchRequest(
+            entity: TempTargetRunStored.entity(),
+            sortDescriptors: [NSSortDescriptor(keyPath: \TempTargetRunStored.startDate, ascending: false)],
+            predicate: NSPredicate.tempTargetRunStoredFromOneDayAgo,
+            animation: .bouncy
+        ) var tempTargetRunStored: FetchedResults<TempTargetRunStored>
 
         private var manualGlucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -76,36 +70,6 @@ extension DataTable {
             return formatter
         }
 
-        private var dateFormatter: DateFormatter {
-            let formatter = DateFormatter()
-            formatter.timeStyle = .short
-            return formatter
-        }
-
-        private var numberFormatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 2
-            return formatter
-        }
-
-        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 {
             ZStack(alignment: .center, content: {
                 VStack {
@@ -125,9 +89,10 @@ extension DataTable {
                         case .treatments: treatmentsList
                         case .glucose: glucoseList
                         case .meals: mealsList
+                        case .adjustments: adjustmentsList
                         }
                     }.scrollContentBackground(.hidden)
-                        .background(color)
+                        .background(appState.trioBackgroundColor(for: colorScheme))
                 }.blur(radius: state.waitForSuggestion ? 8 : 0)
 
                 // Show custom progress view
@@ -136,7 +101,7 @@ extension DataTable {
                     CustomProgressView(text: progressText.rawValue)
                 }
             })
-                .background(color)
+                .background(appState.trioBackgroundColor(for: colorScheme))
                 .onAppear(perform: configureView)
                 .onDisappear {
                     state.carbEntryDeleted = false
@@ -246,6 +211,129 @@ extension DataTable {
             }.listRowBackground(Color.chart)
         }
 
+        private var adjustmentsList: some View {
+            List {
+                HStack {
+                    Text("Adjustment").foregroundStyle(.secondary)
+                    Spacer()
+                }
+                if !combinedAdjustments.isEmpty {
+                    ForEach(combinedAdjustments) { item in
+                        adjustmentView(for: item)
+                    }
+                } else {
+                    HStack {
+                        Text("No data.")
+                    }
+                }
+            }
+            .listRowBackground(Color.chart)
+        }
+
+        private var combinedAdjustments: [AdjustmentItem] {
+            let overrides = overrideRunStored.map { override -> AdjustmentItem in
+                AdjustmentItem(
+                    id: override.objectID,
+                    name: override.name ?? "Override",
+                    startDate: override.startDate ?? Date(),
+                    endDate: override.endDate ?? Date(),
+                    target: override.target?.decimalValue,
+                    type: .override
+                )
+            }
+
+            let tempTargets = tempTargetRunStored.map { tempTarget -> AdjustmentItem in
+                AdjustmentItem(
+                    id: tempTarget.objectID,
+                    name: tempTarget.name ?? "Temp Target",
+                    startDate: tempTarget.startDate ?? Date(),
+                    endDate: tempTarget.endDate ?? Date(),
+                    target: tempTarget.target?.decimalValue,
+                    type: .tempTarget
+                )
+            }
+
+            let combined = overrides + tempTargets
+            return combined.sorted(by: { $0.startDate > $1.startDate })
+        }
+
+        private struct AdjustmentItem: Identifiable {
+            let id: NSManagedObjectID
+            let name: String
+            let startDate: Date
+            let endDate: Date
+            let target: Decimal?
+            let type: AdjustmentType
+        }
+
+        private enum AdjustmentType {
+            case override
+            case tempTarget
+
+            var symbolName: String {
+                switch self {
+                case .override:
+                    return "clock.arrow.2.circlepath"
+                case .tempTarget:
+                    return "target"
+                }
+            }
+
+            var symbolColor: Color {
+                switch self {
+                case .override:
+                    return .orange
+                case .tempTarget:
+                    return .blue
+                }
+            }
+        }
+
+        @ViewBuilder private func adjustmentView(for item: AdjustmentItem) -> some View {
+            let formattedDates =
+                "\(Formatter.dateFormatter.string(from: item.startDate)) - \(Formatter.dateFormatter.string(from: item.endDate))"
+
+            let targetDescription: String = {
+                guard let target = item.target, target != 0 else {
+                    return ""
+                }
+                return "\(state.units == .mgdL ? target : target.asMmolL) \(state.units.rawValue)"
+            }()
+
+            let labels: [String] = [
+                targetDescription,
+                formattedDates
+            ].filter { !$0.isEmpty }
+
+            ZStack(alignment: .trailing) {
+                HStack {
+                    VStack(alignment: .leading) {
+                        HStack {
+                            Image(systemName: item.type.symbolName)
+                                .foregroundStyle(item.type == .override ? Color.purple : Color.green)
+                            Text(item.name)
+                                .font(.headline)
+                            Spacer()
+                        }
+                        HStack(spacing: 5) {
+                            ForEach(labels, id: \.self) { label in
+                                Text(label)
+                                if label != labels.last {
+                                    Divider()
+                                }
+                            }
+                            Spacer()
+                        }
+                        .padding(.top, 2)
+                        .foregroundColor(.secondary)
+                        .font(.caption)
+                    }
+                    .contentShape(Rectangle())
+                }
+            }
+            .padding(.vertical, 8)
+        }
+
         private var glucoseList: some View {
             List {
                 HStack {
@@ -267,7 +355,7 @@ extension DataTable {
 
                             Spacer()
 
-                            Text(dateFormatter.string(from: glucose.date ?? Date()))
+                            Text(Formatter.dateFormatter.string(from: glucose.date ?? Date()))
                         }.swipeActions {
                             Button(
                                 "Delete",
@@ -277,9 +365,9 @@ extension DataTable {
                                     alertGlucoseToDelete = glucose
 
                                     alertTitle = "Delete Glucose?"
-                                    alertMessage = dateFormatter
+                                    alertMessage = Formatter.dateFormatter
                                         .string(from: glucose.date ?? Date()) + ", " +
-                                        (numberFormatter.string(for: glucose.glucose) ?? "0")
+                                        (Formatter.decimalFormatterWithTwoFractionDigits.string(for: glucose.glucose) ?? "0")
 
                                     isRemoveHistoryItemAlertPresented = true
                                 }
@@ -369,7 +457,7 @@ extension DataTable {
                                 .manualGlucose > limitHigh ? Color(.systemGray4) : Color(.systemBlue)
                         )
                         .tint(.white)
-                    }.scrollContentBackground(.hidden).background(color)
+                    }.scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
                 }
                 .onAppear(perform: configureView)
                 .navigationTitle("Add Glucose")
@@ -399,8 +487,11 @@ extension DataTable {
                 if let bolus = item.bolus, let amount = bolus.amount {
                     Image(systemName: "circle.fill").foregroundColor(Color.insulin)
                     Text(bolus.isSMB ? "SMB" : item.type ?? "Bolus")
-                    Text((insulinFormatter.string(from: amount) ?? "0") + NSLocalizedString(" U", comment: "Insulin unit"))
-                        .foregroundColor(.secondary)
+                    Text(
+                        (Formatter.decimalFormatterWithTwoFractionDigits.string(from: amount) ?? "0") +
+                            NSLocalizedString(" U", comment: "Insulin unit")
+                    )
+                    .foregroundColor(.secondary)
                     if bolus.isExternal {
                         Text(NSLocalizedString("External", comment: "External Insulin")).foregroundColor(.secondary)
                     }
@@ -408,7 +499,7 @@ extension DataTable {
                     Image(systemName: "circle.fill").foregroundColor(Color.insulin.opacity(0.4))
                     Text("Temp Basal")
                     Text(
-                        (insulinFormatter.string(from: rate) ?? "0") +
+                        (Formatter.decimalFormatterWithTwoFractionDigits.string(from: rate) ?? "0") +
                             NSLocalizedString(" U/hr", comment: "Unit insulin per hour")
                     )
                     .foregroundColor(.secondary)
@@ -420,7 +511,7 @@ extension DataTable {
                     Text(item.type ?? "Pump Event")
                 }
                 Spacer()
-                Text(dateFormatter.string(from: item.timestamp ?? Date())).moveDisabled(true)
+                Text(Formatter.dateFormatter.string(from: item.timestamp ?? Date())).moveDisabled(true)
             }
             .swipeActions {
                 if item.bolus != nil {
@@ -431,9 +522,9 @@ extension DataTable {
                         action: {
                             alertTreatmentToDelete = item
                             alertTitle = "Delete Insulin?"
-                            alertMessage = dateFormatter
+                            alertMessage = Formatter.dateFormatter
                                 .string(from: item.timestamp ?? Date()) + ", " +
-                                (insulinFormatter.string(from: item.bolus?.amount ?? 0) ?? "0") +
+                                (Formatter.decimalFormatterWithTwoFractionDigits.string(from: item.bolus?.amount ?? 0) ?? "0") +
                                 NSLocalizedString(" U", comment: "Insulin unit")
 
                             if let bolus = item.bolus {
@@ -471,19 +562,22 @@ extension DataTable {
                     if meal.isFPU {
                         Image(systemName: "circle.fill").foregroundColor(Color.orange.opacity(0.5))
                         Text("Fat / Protein")
-                        Text((numberFormatter.string(for: meal.carbs) ?? "0") + NSLocalizedString(" g", comment: "gram of carbs"))
+                        Text(
+                            (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
+                                NSLocalizedString(" g", comment: "gram of carbs")
+                        )
                     } else {
                         Image(systemName: "circle.fill").foregroundColor(Color.loopYellow)
                         Text("Carbs")
                         Text(
-                            (numberFormatter.string(for: meal.carbs) ?? "0") +
+                            (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
                                 NSLocalizedString(" g", comment: "gram of carb equilvalents")
                         )
                     }
 
                     Spacer()
 
-                    Text(dateFormatter.string(from: meal.date ?? Date()))
+                    Text(Formatter.dateFormatter.string(from: meal.date ?? Date()))
                         .moveDisabled(true)
                 }
                 if let note = meal.note, note != "" {
@@ -504,8 +598,9 @@ extension DataTable {
 
                         if !meal.isFPU {
                             alertTitle = "Delete Carbs?"
-                            alertMessage = dateFormatter
-                                .string(from: meal.date ?? Date()) + ", " + (numberFormatter.string(for: meal.carbs) ?? "0") +
+                            alertMessage = Formatter.dateFormatter
+                                .string(from: meal.date ?? Date()) + ", " +
+                                (Formatter.decimalFormatterWithTwoFractionDigits.string(for: meal.carbs) ?? "0") +
                                 NSLocalizedString(" g", comment: "gram of carbs")
                         } else {
                             alertTitle = "Delete Carb Equivalents?"
@@ -538,7 +633,7 @@ extension DataTable {
         // MARK: - Format glucose
 
         private func formatGlucose(_ value: Decimal, isManual: Bool) -> String {
-            let formatter = isManual ? manualGlucoseFormatter : glucoseFormatter
+            let formatter = isManual ? manualGlucoseFormatter : Formatter.glucoseFormatter(for: state.units)
             let glucoseValue = state.units == .mmolL ? value.asMmolL : value
             let formattedValue = formatter.string(from: glucoseValue as NSNumber) ?? "--"
 

+ 0 - 0
FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift


Некоторые файлы не были показаны из-за большого количества измененных файлов