Jelajahi Sumber

Merge pull request #97 from dnzxy/tempTargets

polscm32 1 tahun lalu
induk
melakukan
931103086a
100 mengubah file dengan 5787 tambahan dan 1089 penghapusan
  1. 164 76
      FreeAPS.xcodeproj/project.pbxproj
  2. 1 1
      FreeAPS/Resources/javascript/bundle/autosens.js
  3. 1 1
      FreeAPS/Resources/javascript/bundle/autotune-prep.js
  4. 1 1
      FreeAPS/Resources/javascript/bundle/determine-basal.js
  5. 1 1
      FreeAPS/Resources/javascript/bundle/iob.js
  6. 1 1
      FreeAPS/Resources/javascript/bundle/meal.js
  7. 2 2
      FreeAPS/Sources/APS/APSManager.swift
  8. 43 6
      FreeAPS/Sources/APS/FetchTreatmentsManager.swift
  9. 112 192
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  10. 5 5
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  11. 1 1
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  12. 15 22
      FreeAPS/Sources/APS/Storage/OverrideStorage.swift
  13. 266 50
      FreeAPS/Sources/APS/Storage/TempTargetsStorage.swift
  14. 20 0
      FreeAPS/Sources/Application/AppState.swift
  15. 19 21
      FreeAPS/Sources/Application/FreeAPSApp.swift
  16. 6 0
      FreeAPS/Sources/Helpers/Decimal+Extensions.swift
  17. 51 0
      FreeAPS/Sources/Helpers/Formatters.swift
  18. 138 28
      FreeAPS/Sources/Helpers/MainChartHelper.swift
  19. 1 1
      FreeAPS/Sources/Helpers/ProgressBar.swift
  20. 1 1
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  21. 1 1
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  22. 1 1
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  23. 1 1
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  24. 1 1
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  25. 1 1
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  26. 1 1
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  27. 1 1
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  28. 1 1
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  29. 1 1
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  30. 1 1
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  31. 1 1
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  32. 1 1
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  33. 1 1
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  34. 1 1
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  35. 1 1
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  36. 1 1
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  37. 1 1
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  38. 1 1
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  39. 1 1
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  40. 1 1
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  41. 1 1
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  42. 1 1
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  43. 1 1
      FreeAPS/Sources/Models/AlertEntry.swift
  44. 1 1
      FreeAPS/Sources/Models/CarbsEntry.swift
  45. 2 2
      FreeAPS/Sources/Models/DecimalPickerSettings.swift
  46. 12 12
      FreeAPS/Sources/Models/Oref2_variables.swift
  47. 1 1
      FreeAPS/Sources/Models/Override.swift
  48. 218 1
      FreeAPS/Sources/Models/Preferences.swift
  49. 13 4
      FreeAPS/Sources/Models/TempTarget.swift
  50. 2 2
      FreeAPS/Sources/Modules/OverrideConfig/OverrideDataFlow.swift
  51. 9 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsProvider.swift
  52. 93 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift
  53. 338 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift
  54. 422 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift
  55. 272 0
      FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift
  56. 707 0
      FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift
  57. 474 0
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift
  58. 635 0
      FreeAPS/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift
  59. 388 0
      FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift
  60. 410 0
      FreeAPS/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift
  61. 19 0
      FreeAPS/Sources/Modules/Adjustments/View/ViewElements/RadioButton.swift
  62. 67 0
      FreeAPS/Sources/Modules/Adjustments/View/ViewElements/TargetPicker.swift
  63. 9 0
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsProvider.swift
  64. 32 67
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/AlgorithmAdvancedSettingsStateModel.swift
  65. 4 19
      FreeAPS/Sources/Modules/AlgorithmAdvancedSettings/View/AlgorithmAdvancedSettingsRootView.swift
  66. 9 32
      FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift
  67. 3 22
      FreeAPS/Sources/Modules/AutosensSettings/View/AutosensSettingsRootView.swift
  68. 2 17
      FreeAPS/Sources/Modules/AutotuneConfig/View/AutotuneConfigRootView.swift
  69. 30 34
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  70. 6 22
      FreeAPS/Sources/Modules/BasalProfileEditor/View/BasalProfileEditorRootView.swift
  71. 15 0
      FreeAPS/Sources/Modules/Base/BaseStateModel.swift
  72. 2 17
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  73. 4 19
      FreeAPS/Sources/Modules/CGM/View/CGMRootView.swift
  74. 2 18
      FreeAPS/Sources/Modules/CalendarEventSettings/View/CalendarEventSettingsRootView.swift
  75. 2 17
      FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift
  76. 3 18
      FreeAPS/Sources/Modules/CarbRatioEditor/View/CarbRatioEditorRootView.swift
  77. 2 17
      FreeAPS/Sources/Modules/ConfigEditor/View/ConfigEditorRootView.swift
  78. 3 0
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  79. 2 2
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  80. 162 67
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  81. 20 49
      FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift
  82. 3 21
      FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift
  83. 3 17
      FreeAPS/Sources/Modules/GeneralSettings/UnitsLimitsSettingsStateModel.swift
  84. 2 18
      FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift
  85. 2 18
      FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift
  86. 2 17
      FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift
  87. 1 0
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  88. 36 24
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift
  89. 114 0
      FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift
  90. 48 59
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  91. 0 6
      FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift
  92. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/CarbView.swift
  93. 114 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift
  94. 14 14
      FreeAPS/Sources/Modules/Home/View/Chart/DummyCharts.swift
  95. 0 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/ForecastView.swift
  96. 0 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift
  97. 1 1
      FreeAPS/Sources/Modules/Home/View/Chart/InsulinView.swift
  98. 68 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift
  99. 111 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift
  100. 0 0
      FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/TempTargets.swift

+ 164 - 76
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,11 +327,15 @@
 		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 */; };
@@ -335,6 +344,7 @@
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.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 +377,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 +458,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 */; };
@@ -466,10 +480,10 @@
 		DD9ECB722CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */; };
 		DD9ECB742CA9A0C300AA7C45 /* RemoteControlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
-		DDD163122C4C689900CD525A /* OverrideStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163112C4C689900CD525A /* OverrideStateModel.swift */; };
-		DDD163142C4C68D300CD525A /* OverrideProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163132C4C68D300CD525A /* OverrideProvider.swift */; };
-		DDD163162C4C690300CD525A /* OverrideDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD163152C4C690300CD525A /* OverrideDataFlow.swift */; };
-		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 +497,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 +509,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 */; };
@@ -634,7 +644,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 +933,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 +954,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,11 +1015,15 @@
 		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>"; };
@@ -1013,6 +1032,7 @@
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.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 +1070,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 +1148,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>"; };
@@ -1146,10 +1170,10 @@
 		DD9ECB6F2CA9A0BA00AA7C45 /* RemoteControlConfigDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfigDataFlow.swift; sourceTree = "<group>"; };
 		DD9ECB732CA9A0C300AA7C45 /* RemoteControlConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteControlConfig.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
-		DDD163112C4C689900CD525A /* OverrideStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStateModel.swift; sourceTree = "<group>"; };
-		DDD163132C4C68D300CD525A /* OverrideProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProvider.swift; sourceTree = "<group>"; };
-		DDD163152C4C690300CD525A /* OverrideDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideDataFlow.swift; sourceTree = "<group>"; };
-		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 +1187,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 +1199,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>"; };
@@ -1343,7 +1363,6 @@
 			children = (
 				118DF7642C5ECBC60067FEB7 /* ApplyOverridePresetIntent.swift */,
 				118DF7652C5ECBC60067FEB7 /* CancelOverrideIntent.swift */,
-				118DF7662C5ECBC60067FEB7 /* ListOverridePresetIntent.swift */,
 				118DF7672C5ECBC60067FEB7 /* OverridePresetEntity.swift */,
 				118DF7682C5ECBC60067FEB7 /* OverridePresetsIntentRequest.swift */,
 			);
@@ -1503,6 +1522,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				DDD163032C4C67B400CD525A /* Adjustments */,
 				DD1745382C55BF8B00211FAC /* AlgorithmAdvancedSettings */,
 				DD1745422C55C5C400211FAC /* AutosensSettings */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
@@ -1527,9 +1547,7 @@
 				5031FE61F63C2A8A8B7674DD /* ManualTempBasal */,
 				19D466A129AA2B0A004D5F33 /* MealSettings */,
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
-				DDD163032C4C67B400CD525A /* OverrideConfig */,
 				99C01B871ACAB3F32CE755C7 /* PumpConfig */,
-				DD9ECB662CA99EFE00AA7C45 /* RemoteControl */,
 				DD9ECB6B2CA99FA400AA7C45 /* RemoteControlConfig */,
 				3811DE3825C9D4A100A708ED /* Settings */,
 				110AEDEA2C51A0AE00615CC9 /* ShortcutsConfig */,
@@ -1652,18 +1670,19 @@
 		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 */,
+				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 +1701,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 +1766,7 @@
 			children = (
 				38E4451D274DB04600EC9A94 /* AppDelegate.swift */,
 				388E595B25AD948C0019842D /* FreeAPSApp.swift */,
+				BD4ED4FC2CF9D5E8000EDC9C /* AppState.swift */,
 			);
 			path = Application;
 			sourceTree = "<group>";
@@ -1835,16 +1854,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>";
@@ -2303,7 +2313,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 +2324,7 @@
 		58645B972CA2D16A008AFCE7 /* HomeStateModel+Setup */ = {
 			isa = PBXGroup;
 			children = (
+				BDA6CC872CAF219800F942F9 /* TempTargetSetup.swift */,
 				58645B982CA2D1A4008AFCE7 /* GlucoseSetup.swift */,
 				58645B9A2CA2D24F008AFCE7 /* CarbSetup.swift */,
 				58645B9C2CA2D275008AFCE7 /* DeterminationSetup.swift */,
@@ -2433,6 +2447,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 +2484,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 +2549,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 +2782,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 +2843,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,6 +2878,10 @@
 		DDE179112C9100FA003CDDB7 /* Classes+Properties */ = {
 			isa = PBXGroup;
 			children = (
+				58A3D54D2C96EFA8003F90FC /* TempTargetStored+CoreDataClass.swift */,
+				58A3D54E2C96EFA8003F90FC /* TempTargetStored+CoreDataProperties.swift */,
+				58A3D54F2C96EFA8003F90FC /* TempTargetRunStored+CoreDataClass.swift */,
+				58A3D5502C96EFA8003F90FC /* TempTargetRunStored+CoreDataProperties.swift */,
 				DDE179322C910127003CDDB7 /* MealPresetStored+CoreDataClass.swift */,
 				DDE179332C910127003CDDB7 /* MealPresetStored+CoreDataProperties.swift */,
 				DDE179342C910127003CDDB7 /* LoopStatRecord+CoreDataClass.swift */,
@@ -2810,8 +2892,6 @@
 				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 */,
@@ -2824,8 +2904,6 @@
 				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 */,
@@ -3267,6 +3345,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 +3353,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 +3366,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 +3410,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 +3419,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 +3450,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 */,
@@ -3410,6 +3494,7 @@
 				BD0B2EF32C5998E600B3298F /* MealPresetView.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 +3502,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 +3520,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 +3531,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 +3569,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 +3594,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 +3610,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 +3621,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 */,
@@ -3604,7 +3689,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 +3700,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 +3708,13 @@
 				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 */,
 				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 */,
@@ -3657,7 +3746,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 +3771,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 */,
@@ -3703,9 +3793,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 */,
@@ -3717,8 +3806,6 @@
 				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 +3817,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 */,

File diff ditekan karena terlalu besar
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autosens.js


File diff ditekan karena terlalu besar
+ 1 - 1
FreeAPS/Resources/javascript/bundle/autotune-prep.js


File diff ditekan karena terlalu besar
+ 1 - 1
FreeAPS/Resources/javascript/bundle/determine-basal.js


File diff ditekan karena terlalu besar
+ 1 - 1
FreeAPS/Resources/javascript/bundle/iob.js


File diff ditekan karena terlalu besar
+ 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(),

+ 43 - 6
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,52 @@ 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) }
-                    if filteredCarbs.isNotEmpty {
-                        await self.carbsStorage.storeCarbs(filteredCarbs, areFetchedFromRemote: true)
+                    // Store carbs if available
+                    let fetchedCarbs = await carbs
+                    if fetchedCarbs.isNotEmpty {
+                        await self.carbsStorage.storeCarbs(fetchedCarbs, areFetchedFromRemote: true)
                     }
 
-                    let filteredTargets = await tempTargets.filter { !($0.enteredBy?.contains(TempTarget.manual) ?? false) }
-                    if filteredTargets.isNotEmpty {
-                        self.tempTargetsStorage.storeTempTargets(filteredTargets)
+                    // Store temp targets if available
+                    let fetchedTargets = await tempTargets
+                    if fetchedTargets.isNotEmpty {
+                        // Sort temp targets by date
+                        let sortedTargets = fetchedTargets.sorted { lhs, rhs in
+                            lhs.createdAt < rhs.createdAt
+                        }
+
+                        // Iterate over all temp targets
+                        for (index, tempTarget) in sortedTargets.enumerated() {
+                            // Skip saving if a Temp Target with the same date already exists
+                            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 of tempTarget
+                            var mutableTempTarget = tempTarget
+
+                            // Set enabled to true only for the last temp target
+                            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)
                     }
                 }
             }

+ 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
+        )
+    }
+
+    func fetchActiveOverrides() -> [OverrideStored] {
+        CoreDataStack.shared.fetchEntities(
+            ofType: OverrideStored.self,
+            onContext: context,
+            predicate: NSPredicate.lastActiveOverride,
+            key: "date",
+            ascending: false,
+            fetchLimit: 1
+        )
+    }
+
+    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]] ?? []
+    }
+}

+ 5 - 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,7 +402,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,
@@ -441,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
                 )
@@ -472,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
                 )

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

@@ -329,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

+ 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
+            )
+    }
+}

+ 19 - 21
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,43 +62,29 @@ 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()))]"
         )
 
-        // Configure global appearance for UITabBar
-        configureTabBarAppearance()
-
         // 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()
     }
 
-    // Function to configure global tab bar appearance
-    private func configureTabBarAppearance() {
-        let appearance = UITabBarAppearance()
-        appearance.configureWithDefaultBackground()
-
-        // Blur the background
-        appearance.backgroundEffect = UIBlurEffect(style: .systemChromeMaterial)
-
-        // Keep background semi-transparent
-        appearance.backgroundColor = UIColor.clear
-
-        UITabBar.appearance().standardAppearance = appearance
-        UITabBar.appearance().scrollEdgeAppearance = appearance
-    }
-
     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
@@ -110,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:

+ 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";

+ 1 - 1
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";

+ 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

+ 1 - 1
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 {

+ 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)

+ 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
+    }
+}

File diff ditekan karena terlalu besar
+ 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)

+ 9 - 32
FreeAPS/Sources/Modules/AutosensSettings/AutosensSettingsStateModel.swift

@@ -2,45 +2,22 @@ import Observation
 import SwiftUI
 
 extension AutosensSettings {
-    @Observable final class StateModel: BaseStateModel<Provider> {
-        @ObservationIgnored @Injected() var settings: SettingsManager!
-        @ObservationIgnored @Injected() var storage: FileStorage!
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Injected() var storage: FileStorage!
 
         var units: GlucoseUnits = .mgdL
 
-        var autosensMax: Decimal = 1.2
-        var autosensMin: Decimal = 0.7
-        var rewindResetsAutosens: Bool = true
-
-        var preferences: Preferences {
-            settingsManager.preferences
-        }
+        @Published var autosensMax: Decimal = 1.2
+        @Published var autosensMin: Decimal = 0.7
+        @Published var rewindResetsAutosens: Bool = true
 
         override func subscribe() {
             units = settingsManager.settings.units
 
-            autosensMax = settings.preferences.autosensMax
-            autosensMin = settings.preferences.autosensMin
-            rewindResetsAutosens = settings.preferences.rewindResetsAutosens
-        }
-
-        var isSettingUnchanged: Bool {
-            preferences.autosensMax == autosensMax &&
-                preferences.autosensMin == autosensMin &&
-                preferences.rewindResetsAutosens == rewindResetsAutosens
-        }
-
-        func saveIfChanged() {
-            if !isSettingUnchanged {
-                var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
-
-                newSettings.autosensMax = autosensMax
-                newSettings.autosensMin = autosensMin
-                newSettings.rewindResetsAutosens = rewindResetsAutosens
-
-                newSettings.timestamp = Date()
-                storage.save(newSettings, as: OpenAPS.Settings.preferences)
-            }
+            subscribePreferencesSetting(\.autosensMax, on: $autosensMax) { autosensMax = $0 }
+            subscribePreferencesSetting(\.autosensMin, on: $autosensMin) { autosensMin = $0 }
+            subscribePreferencesSetting(\.rewindResetsAutosens, on: $rewindResetsAutosens) { rewindResetsAutosens = $0 }
         }
     }
 }

+ 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
 
         var body: some View {
             List {
@@ -107,13 +91,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
         }
     }
 }

+ 6 - 22
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()
@@ -164,9 +149,9 @@ 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: {
@@ -183,7 +168,7 @@ extension BasalProfileEditor {
             .onAppear {
                 configureView()
                 state.validate()
-                state.caluclateChartData()
+                state.calculateChartData()
             }
         }
 
@@ -219,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)
         }
@@ -249,8 +234,7 @@ extension BasalProfileEditor {
         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)

+ 4 - 19
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,7 +195,7 @@ 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)
@@ -246,10 +231,10 @@ extension CGM {
                         )
                     }
                 }
-                .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)

+ 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")

+ 3 - 18
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()
@@ -109,7 +94,7 @@ extension CarbRatioEditor {
                 }.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)
@@ -160,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)
         }

+ 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))
         }
     }
 }

+ 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")

+ 2 - 2
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -114,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
@@ -166,7 +166,7 @@ extension DataTable {
                                 withSyncId: id,
                                 carbs: Decimal(carbEntry.carbs),
                                 at: entryDate,
-                                enteredBy: CarbsEntry.manual
+                                enteredBy: CarbsEntry.local
                             )
                         }
                     }

+ 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) ?? "--"
 

+ 20 - 49
FreeAPS/Sources/Modules/DynamicSettings/DynamicSettingsStateModel.swift

@@ -2,61 +2,32 @@ import Observation
 import SwiftUI
 
 extension DynamicSettings {
-    @Observable final class StateModel: BaseStateModel<Provider> {
-        @ObservationIgnored @Injected() var settings: SettingsManager!
-        @ObservationIgnored @Injected() var storage: FileStorage!
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var settings: SettingsManager!
+        @Injected() var storage: FileStorage!
 
-        var useNewFormula: Bool = false
-        var enableDynamicCR: Bool = false
-        var sigmoid: Bool = false
-        var adjustmentFactor: Decimal = 0.8
-        var adjustmentFactorSigmoid: Decimal = 0.5
-        var weightPercentage: Decimal = 0.65
-        var tddAdjBasal: Bool = false
-        var threshold_setting: Decimal = 60
-        var units: GlucoseUnits = .mgdL
+        @Published var useNewFormula: Bool = false
+        @Published var enableDynamicCR: Bool = false
+        @Published var sigmoid: Bool = false
+        @Published var adjustmentFactor: Decimal = 0.8
+        @Published var adjustmentFactorSigmoid: Decimal = 0.5
+        @Published var weightPercentage: Decimal = 0.65
+        @Published var tddAdjBasal: Bool = false
+        @Published var threshold_setting: Decimal = 60
 
-        var preferences: Preferences {
-            settingsManager.preferences
-        }
+        var units: GlucoseUnits = .mgdL
 
         override func subscribe() {
             units = settingsManager.settings.units
-            useNewFormula = settings.preferences.useNewFormula
-            enableDynamicCR = settings.preferences.enableDynamicCR
-            sigmoid = settings.preferences.sigmoid
-            adjustmentFactor = settings.preferences.adjustmentFactor
-            adjustmentFactorSigmoid = settings.preferences.adjustmentFactorSigmoid
-            weightPercentage = settings.preferences.weightPercentage
-            tddAdjBasal = settings.preferences.tddAdjBasal
-            threshold_setting = settings.preferences.threshold_setting
-        }
-
-        var unChanged: Bool {
-            preferences.enableDynamicCR == enableDynamicCR &&
-                preferences.adjustmentFactor == adjustmentFactor &&
-                preferences.sigmoid == sigmoid &&
-                preferences.adjustmentFactorSigmoid == adjustmentFactorSigmoid &&
-                preferences.tddAdjBasal == tddAdjBasal &&
-                preferences.threshold_setting == threshold_setting &&
-                preferences.useNewFormula == useNewFormula &&
-                preferences.weightPercentage == weightPercentage
-        }
 
-        func saveIfChanged() {
-            if !unChanged {
-                var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
-                newSettings.enableDynamicCR = enableDynamicCR
-                newSettings.adjustmentFactor = adjustmentFactor
-                newSettings.sigmoid = sigmoid
-                newSettings.adjustmentFactorSigmoid = adjustmentFactorSigmoid
-                newSettings.tddAdjBasal = tddAdjBasal
-                newSettings.threshold_setting = threshold_setting
-                newSettings.useNewFormula = useNewFormula
-                newSettings.weightPercentage = weightPercentage
-                newSettings.timestamp = Date()
-                storage.save(newSettings, as: OpenAPS.Settings.preferences)
-            }
+            subscribePreferencesSetting(\.useNewFormula, on: $useNewFormula) { useNewFormula = $0 }
+            subscribePreferencesSetting(\.enableDynamicCR, on: $enableDynamicCR) { enableDynamicCR = $0 }
+            subscribePreferencesSetting(\.sigmoid, on: $sigmoid) { sigmoid = $0 }
+            subscribePreferencesSetting(\.adjustmentFactor, on: $adjustmentFactor) { adjustmentFactor = $0 }
+            subscribePreferencesSetting(\.adjustmentFactorSigmoid, on: $adjustmentFactorSigmoid) { adjustmentFactorSigmoid = $0 }
+            subscribePreferencesSetting(\.weightPercentage, on: $weightPercentage) { weightPercentage = $0 }
+            subscribePreferencesSetting(\.tddAdjBasal, on: $tddAdjBasal) { tddAdjBasal = $0 }
+            subscribePreferencesSetting(\.tddAdjBasal, on: $tddAdjBasal) { tddAdjBasal = $0 }
         }
     }
 }

+ 3 - 21
FreeAPS/Sources/Modules/DynamicSettings/View/DynamicSettingsRootView.swift

@@ -4,7 +4,7 @@ import Swinject
 extension DynamicSettings {
     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?
@@ -21,22 +21,7 @@ extension DynamicSettings {
         }
 
         @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()
@@ -214,13 +199,10 @@ extension DynamicSettings {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationBarTitle("Dynamic Settings")
             .navigationBarTitleDisplayMode(.automatic)
-            .onDisappear {
-                state.saveIfChanged()
-            }
         }
     }
 }

+ 3 - 17
FreeAPS/Sources/Modules/GeneralSettings/UnitsLimitsSettingsStateModel.swift

@@ -29,10 +29,11 @@ extension UnitsLimitsSettings {
                 unitsIndex = $0 == .mgdL ? 0 : 1
             }
 
+            subscribePreferencesSetting(\.maxIOB, on: $maxIOB) { maxIOB = $0 }
+            subscribePreferencesSetting(\.maxCOB, on: $maxCOB) { maxCOB = $0 }
+
             maxBasal = pumpSettings.maxBasal
             maxBolus = pumpSettings.maxBolus
-            maxIOB = settings.preferences.maxIOB
-            maxCOB = settings.preferences.maxCOB
         }
 
         var isPumpSettingUnchanged: Bool {
@@ -40,22 +41,7 @@ extension UnitsLimitsSettings {
                 pumpSettings.maxBolus == maxBolus
         }
 
-        var isSettingUnchanged: Bool {
-            preferences.maxIOB == maxIOB &&
-                preferences.maxCOB == maxCOB
-        }
-
         func saveIfChanged() {
-            if !isSettingUnchanged {
-                var newSettings = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self) ?? Preferences()
-
-                newSettings.maxIOB = maxIOB
-                newSettings.maxCOB = maxCOB
-
-                newSettings.timestamp = Date()
-                storage.save(newSettings, as: OpenAPS.Settings.preferences)
-            }
-
             if !isPumpSettingUnchanged {
                 let settings = PumpSettings(
                     insulinActionCurve: pumpSettings.insulinActionCurve,

+ 2 - 18
FreeAPS/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -14,23 +14,7 @@ extension UnitsLimitsSettings {
 
         @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 {
@@ -131,7 +115,7 @@ extension UnitsLimitsSettings {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("General")
             .navigationBarTitleDisplayMode(.automatic)

+ 2 - 18
FreeAPS/Sources/Modules/GlucoseNotificationSettings/View/GlucoseNotificationSettingsRootView.swift

@@ -36,23 +36,7 @@ extension GlucoseNotificationSettings {
         }
 
         @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 {
             Form {
@@ -210,7 +194,7 @@ extension GlucoseNotificationSettings {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationBarTitle("Trio Notifications")
             .navigationBarTitleDisplayMode(.automatic)

+ 2 - 17
FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift

@@ -14,22 +14,7 @@ extension AppleHealthKit {
         @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 {
             Form {
@@ -84,7 +69,7 @@ extension AppleHealthKit {
                     sheetTitle: "Help"
                 )
             }
-            .scrollContentBackground(.hidden).background(color)
+            .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
             .onAppear(perform: configureView)
             .navigationTitle("Apple Health")
             .navigationBarTitleDisplayMode(.automatic)

+ 1 - 0
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -14,4 +14,5 @@ protocol HomeProvider: Provider {
     func pumpReservoir() -> Decimal?
     func tempTarget() -> TempTarget?
     func announcement(_ hours: Int) -> [Announcement]
+    func getBGTarget() async -> BGTargets
 }

+ 36 - 24
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/OverrideSetup.swift

@@ -38,13 +38,6 @@ extension Home.StateModel {
         return TimeInterval(overrideDuration * 60) // return seconds
     }
 
-    @MainActor func calculateTarget(override: OverrideStored) -> Decimal {
-        guard let overrideTarget = override.target, overrideTarget != 0 else {
-            return 100 // default
-        }
-        return overrideTarget.decimalValue
-    }
-
     // Setup expired Overrides
     func setupOverrideRunStored() {
         Task {
@@ -76,23 +69,42 @@ extension Home.StateModel {
         overrideRunStored = objects
     }
 
-    @MainActor func saveToOverrideRunStored(withID id: NSManagedObjectID) async {
-        await viewContext.perform {
-            do {
-                guard let object = try self.viewContext.existingObject(with: id) as? OverrideStored else { return }
-
-                let newOverrideRunStored = OverrideRunStored(context: self.viewContext)
-                newOverrideRunStored.id = UUID()
-                newOverrideRunStored.name = object.name
-                newOverrideRunStored.startDate = object.date ?? .distantPast
-                newOverrideRunStored.endDate = Date()
-                newOverrideRunStored.target = NSDecimalNumber(decimal: self.calculateTarget(override: object))
-                newOverrideRunStored.override = object
-                newOverrideRunStored.isUploadedToNS = false
-
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to initialize a new Override Run Object")
-            }
+    /// Cancels the running Override, creates an entry in the OverrideRunStored Core Data entity and posts a custom notification so that the AdjustmentsView gets updated
+    @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
+        do {
+            guard let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored else { return }
+
+            profileToCancel.enabled = false
+
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+
+            await saveToOverrideRunStored(object: profileToCancel)
+
+            Foundation.NotificationCenter.default.post(name: .didUpdateOverrideConfiguration, object: nil)
+        } catch let error as NSError {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile with error: \(error)")
+        }
+    }
+
+    /// We can safely pass the NSManagedObject  as we are doing everything on the Main Actor
+    @MainActor func saveToOverrideRunStored(object: OverrideStored) async {
+        let newOverrideRunStored = OverrideRunStored(context: viewContext)
+        newOverrideRunStored.id = UUID()
+        newOverrideRunStored.name = object.name
+        newOverrideRunStored.startDate = object.date ?? .distantPast
+        newOverrideRunStored.endDate = Date()
+        newOverrideRunStored.target = NSDecimalNumber(decimal: overrideStorage.calculateTarget(override: object))
+        newOverrideRunStored.override = object
+        newOverrideRunStored.isUploadedToNS = false
+
+        do {
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+        } catch let error as NSError {
+            debugPrint(
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save an Override to the OverrideRunStored entity with error: \(error)"
+            )
         }
     }
 }

+ 114 - 0
FreeAPS/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -0,0 +1,114 @@
+import CoreData
+import Foundation
+
+extension Home.StateModel {
+    func setupTempTargetsStored() {
+        Task {
+            let ids = await self.fetchTempTargets()
+            let tempTargetObjects: [TempTargetStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateTempTargetsArray(with: tempTargetObjects)
+        }
+    }
+
+    private func fetchTempTargets() async -> [NSManagedObjectID] {
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetStored.self,
+            onContext: tempTargetFetchContext,
+            predicate: NSPredicate.lastActiveTempTarget,
+            key: "date",
+            ascending: false
+        )
+
+        return await tempTargetFetchContext.perform {
+            guard let fetchedResults = results as? [TempTargetStored] else { return [] }
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateTempTargetsArray(with objects: [TempTargetStored]) {
+        tempTargetStored = objects
+    }
+
+    // Setup expired TempTargets
+    func setupTempTargetsRunStored() {
+        Task {
+            let ids = await self.fetchTempTargetRunStored()
+            let tempTargetRunObjects: [TempTargetRunStored] = await CoreDataStack.shared
+                .getNSManagedObject(with: ids, context: viewContext)
+            await updateTempTargetRunStoredArray(with: tempTargetRunObjects)
+        }
+    }
+
+    private func fetchTempTargetRunStored() async -> [NSManagedObjectID] {
+        let predicate = NSPredicate(format: "startDate >= %@", Date.oneDayAgo as NSDate)
+        let results = await CoreDataStack.shared.fetchEntitiesAsync(
+            ofType: TempTargetRunStored.self,
+            onContext: tempTargetFetchContext,
+            predicate: predicate,
+            key: "startDate",
+            ascending: false
+        )
+
+        return await tempTargetFetchContext.perform {
+            guard let fetchedResults = results as? [TempTargetRunStored] else { return [] }
+            return fetchedResults.map(\.objectID)
+        }
+    }
+
+    @MainActor private func updateTempTargetRunStoredArray(with objects: [TempTargetRunStored]) {
+        tempTargetRunStored = objects
+    }
+
+    @MainActor func cancelTempTarget(withID id: NSManagedObjectID) async {
+        do {
+            guard let profileToCancel = try viewContext.existingObject(with: id) as? TempTargetStored else { return }
+
+            profileToCancel.enabled = false
+
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+
+            // Do not save Cancel-Temp Targets from Nightscout to RunStoredEntity
+            if profileToCancel.duration != 0, profileToCancel.target != 0 {
+                await saveToTempTargetRunStored(object: profileToCancel)
+            }
+
+            // We also need to update the storage for temp targets
+            tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date())])
+
+            Foundation.NotificationCenter.default.post(name: .didUpdateTempTargetConfiguration, object: nil)
+        } catch let error as NSError {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Temp Target with error: \(error)")
+        }
+    }
+
+    @MainActor func saveToTempTargetRunStored(object: TempTargetStored) async {
+        let newTempTargetRunStored = TempTargetRunStored(context: viewContext)
+        newTempTargetRunStored.id = UUID()
+        newTempTargetRunStored.name = object.name
+        newTempTargetRunStored.startDate = object.date ?? .distantPast
+        newTempTargetRunStored.endDate = Date()
+        newTempTargetRunStored.target = object.target ?? 0
+        newTempTargetRunStored.tempTarget = object
+        newTempTargetRunStored.isUploadedToNS = false
+
+        do {
+            guard viewContext.hasChanges else { return }
+            try viewContext.save()
+        } catch let error as NSError {
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target with error: \(error)")
+        }
+    }
+
+    func computeAdjustedPercentage(halfBasalTargetValue: Decimal, tempTargetValue: Decimal) -> Int {
+        let normalTarget: Decimal = 100
+        let deviationFromNormal = halfBasalTargetValue - normalTarget
+
+        let adjustmentFactor = deviationFromNormal + (tempTargetValue - normalTarget)
+        let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
+            adjustmentFactor
+
+        return Int(Double(min(adjustmentRatio, maxValue) * 100).rounded())
+    }
+}

+ 48 - 59
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -15,6 +15,8 @@ extension Home {
         @ObservationIgnored @Injected() var determinationStorage: DeterminationStorage!
         @ObservationIgnored @Injected() var glucoseStorage: GlucoseStorage!
         @ObservationIgnored @Injected() var carbsStorage: CarbsStorage!
+        @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
+        @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
         var manualGlucose: [BloodGlucose] = []
@@ -36,6 +38,11 @@ extension Home {
         var pumpName = ""
         var pumpExpiresAtDate: Date?
         var tempTarget: TempTarget?
+        var highTTraisesSens: Bool = false
+        var lowTTlowersSens: Bool = false
+        var isExerciseModeActive: Bool = false
+        var settingHalfBasalTarget: Decimal = 160
+        var percentage: Int = 100
         var setupPump = false
         var errorMessage: String?
         var errorDate: Date?
@@ -79,13 +86,14 @@ extension Home {
         var lastPumpBolus: PumpEventStored?
         var overrides: [OverrideStored] = []
         var overrideRunStored: [OverrideRunStored] = []
+        var tempTargetStored: [TempTargetStored] = []
+        var tempTargetRunStored: [TempTargetRunStored] = []
         var isOverrideCancelled: Bool = false
         var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
         var pumpStatusHighlightMessage: String?
         var cgmAvailable: Bool = false
         var showCarbsRequiredBadge: Bool = true
         private(set) var setupPumpType: PumpConfig.PumpType = .minimed
-
         var minForecast: [Int] = []
         var maxForecast: [Int] = []
         var minCount: Int = 12 // count of Forecasts drawn in 5 min distances, i.e. 12 means a min of 1 hour
@@ -107,6 +115,7 @@ extension Home {
         let determinationFetchContext = CoreDataStack.shared.newTaskContext()
         let pumpHistoryFetchContext = CoreDataStack.shared.newTaskContext()
         let overrideFetchContext = CoreDataStack.shared.newTaskContext()
+        let tempTargetFetchContext = CoreDataStack.shared.newTaskContext()
         let batteryFetchContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -160,15 +169,9 @@ extension Home {
                         self.setupBasalProfile()
                     }
                     group.addTask {
-                        self.setupTempTargets()
-                    }
-                    group.addTask {
                         self.setupReservoir()
                     }
                     group.addTask {
-                        self.setupAnnouncements()
-                    }
-                    group.addTask {
                         self.setupCurrentPumpTimezone()
                     }
                     group.addTask {
@@ -178,6 +181,12 @@ extension Home {
                         self.setupOverrideRunStored()
                     }
                     group.addTask {
+                        self.setupTempTargetsStored()
+                    }
+                    group.addTask {
+                        self.setupTempTargetsRunStored()
+                    }
+                    group.addTask {
                         await self.setupSettings()
                     }
                     group.addTask {
@@ -243,22 +252,31 @@ extension Home {
                 guard let self = self else { return }
                 self.setupOverrideRunStored()
             }.store(in: &subscriptions)
+
+            coreDataPublisher?.filterByEntityName("TempTargetStored").sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupTempTargetsStored()
+            }.store(in: &subscriptions)
+
+            coreDataPublisher?.filterByEntityName("TempTargetRunStored").sink { [weak self] _ in
+                guard let self = self else { return }
+                self.setupTempTargetsRunStored()
+            }.store(in: &subscriptions)
         }
 
         private func registerObservers() {
             broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
+            broadcaster.register(PreferencesObserver.self, observer: self)
             broadcaster.register(PumpSettingsObserver.self, observer: self)
             broadcaster.register(BasalProfileObserver.self, observer: self)
-            broadcaster.register(TempTargetsObserver.self, observer: self)
             broadcaster.register(PumpReservoirObserver.self, observer: self)
             broadcaster.register(PumpDeactivatedObserver.self, observer: self)
 
             timer.eventHandler = {
                 DispatchQueue.main.async { [weak self] in
                     self?.timerDate = Date()
-                    self?.setupCurrentTempTarget()
                 }
             }
             timer.resume()
@@ -320,6 +338,13 @@ extension Home {
                 .store(in: &lifetime)
         }
 
+        private enum SettingType {
+            case basal
+            case carbRatio
+            case bgTarget
+            case isf
+        }
+
         @MainActor private func setupSettings() async {
             units = settingsManager.settings.units
             allowManualTemp = !settingsManager.settings.closedLoop
@@ -327,7 +352,6 @@ extension Home {
             lastLoopDate = apsManager.lastLoopDate
             alarm = provider.glucoseStorage.alarm
             manualTempBasal = apsManager.isManualTempBasal
-            setupCurrentTempTarget()
             isSmoothingEnabled = settingsManager.settings.smoothGlucose
             glucoseColorScheme = settingsManager.settings.glucoseColorScheme
             maxValue = settingsManager.preferences.autosensMax
@@ -341,6 +365,11 @@ extension Home {
             cgmAvailable = fetchGlucoseManager.cgmGlucoseSourceType != CGMType.none
             showCarbsRequiredBadge = settingsManager.settings.showCarbsRequiredBadge
             forecastDisplayType = settingsManager.settings.forecastDisplayType
+            isExerciseModeActive = settingsManager.preferences.exerciseMode
+            highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
+            lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
+            settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+            maxValue = settingsManager.preferences.autosensMax
         }
 
         func addPump(_ type: PumpConfig.PumpType) {
@@ -382,22 +411,6 @@ extension Home {
             }
         }
 
-        @MainActor func cancelOverride(withID id: NSManagedObjectID) async {
-            do {
-                let profileToCancel = try viewContext.existingObject(with: id) as? OverrideStored
-                profileToCancel?.enabled = false
-
-                await saveToOverrideRunStored(withID: id)
-
-                guard viewContext.hasChanges else { return }
-                try viewContext.save()
-
-                Foundation.NotificationCenter.default.post(name: .willUpdateOverrideConfiguration, object: nil)
-            } catch {
-                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel Profile")
-            }
-        }
-
         func calculateTINS() -> String {
             let startTime = calculateStartTime(hours: Int(hours))
 
@@ -462,21 +475,6 @@ extension Home {
             }
         }
 
-        private func setupTempTargets() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.manualTempBasal = self.apsManager.isManualTempBasal
-                self.tempTargets = self.provider.tempTargets(hours: self.filteredHours)
-            }
-        }
-
-        private func setupAnnouncements() {
-            DispatchQueue.main.async { [weak self] in
-                guard let self = self else { return }
-                self.announcement = self.provider.announcement(self.filteredHours)
-            }
-        }
-
         private func setupReservoir() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -484,10 +482,6 @@ extension Home {
             }
         }
 
-        private func setupCurrentTempTarget() {
-            tempTarget = provider.tempTarget()
-        }
-
         private func setupCurrentPumpTimezone() {
             DispatchQueue.main.async { [weak self] in
                 guard let self = self else { return }
@@ -546,15 +540,6 @@ extension Home {
         func openCGM() {
             router.mainSecondaryModalView.send(router.view(for: .cgmDirect))
         }
-
-        func infoPanelTTPercentage(_ hbt_: Double, _ target: Decimal) -> Decimal {
-            guard hbt_ != 0 || target != 0 else {
-                return 0
-            }
-            let c = Decimal(hbt_ - 100)
-            let ratio = min(c / (target + c - 100), maxValue)
-            return (ratio * 100)
-        }
     }
 }
 
@@ -562,9 +547,9 @@ extension Home.StateModel:
     GlucoseObserver,
     DeterminationObserver,
     SettingsObserver,
+    PreferencesObserver,
     PumpSettingsObserver,
     BasalProfileObserver,
-    TempTargetsObserver,
     PumpReservoirObserver,
     PumpTimeZoneObserver,
     PumpDeactivatedObserver
@@ -602,6 +587,14 @@ extension Home.StateModel:
         setupBatteryArray()
     }
 
+    func preferencesDidChange(_: Preferences) {
+        maxValue = settingsManager.preferences.autosensMax
+        settingHalfBasalTarget = settingsManager.preferences.halfBasalExerciseTarget
+        highTTraisesSens = settingsManager.preferences.highTemptargetRaisesSensitivity
+        isExerciseModeActive = settingsManager.preferences.exerciseMode
+        lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
+    }
+
     // TODO: is this ever really triggered? react to MOC changes?
     func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
         displayPumpStatusHighlightMessage()
@@ -616,10 +609,6 @@ extension Home.StateModel:
         setupBasalProfile()
     }
 
-    func tempTargetsDidUpdate(_: [TempTarget]) {
-        setupTempTargets()
-    }
-
     func pumpReservoirDidChange(_: Decimal) {
         setupReservoir()
         displayPumpStatusHighlightMessage()

+ 0 - 6
FreeAPS/Sources/Modules/Home/View/Chart/BasalChart.swift

@@ -32,12 +32,6 @@ extension MainChartView {
             .onChange(of: state.maxBasal) {
                 calculateBasals()
             }
-            .onChange(of: state.autotunedBasalProfile) {
-                calculateBasals()
-            }
-            .onChange(of: state.basalProfile) {
-                calculateBasals()
-            }
             .frame(minHeight: geo.size.height * 0.05)
             .frame(width: fullWidth(viewWidth: screenSize.width))
             .chartXScale(domain: startMarker ... endMarker)

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Chart/CarbView.swift

@@ -39,7 +39,7 @@ struct CarbView: ChartContent {
                         .rotationEffect(.degrees(180))
                 }
                 .annotation(position: .bottom) {
-                    Text(MainChartHelper.carbsFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
+                    Text(Formatter.integerFormatter.string(from: carbAmount as NSNumber)!).font(.caption2)
                         .foregroundStyle(Color.primary)
                 }
             }

+ 114 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/CobIobChart.swift

@@ -0,0 +1,114 @@
+import Charts
+import Foundation
+import SwiftUI
+
+extension MainChartView {
+    var cobIobChart: some View {
+        Chart {
+            drawCurrentTimeMarker()
+            drawCOBIOBChart()
+
+            if let selectedCOBValue {
+                drawSelectedInnerPoint(
+                    xValue: selectedCOBValue.deliverAt ?? Date.now,
+                    yValue: Double(selectedCOBValue.cob),
+                    axis: "COB"
+                )
+                drawSelectedOuterPoint(
+                    xValue: selectedCOBValue.deliverAt ?? Date.now,
+                    yValue: Double(selectedCOBValue.cob),
+                    axis: "IOB",
+                    color: Color.orange
+                )
+            }
+
+            if let selectedIOBValue {
+                let rawAmount = selectedIOBValue.iob?.doubleValue ?? 0
+                let amount: Double = rawAmount > 0 ? rawAmount * 8 : rawAmount * 9
+
+                drawSelectedInnerPoint(
+                    xValue: selectedIOBValue.deliverAt ?? Date.now,
+                    yValue: amount,
+                    axis: "COB"
+                )
+                drawSelectedOuterPoint(
+                    xValue: selectedIOBValue.deliverAt ?? Date.now,
+                    yValue: amount,
+                    axis: "IOB",
+                    color: Color.darkerBlue
+                )
+            }
+        }
+        .chartForegroundStyleScale([
+            "COB": Color.orange,
+            "IOB": Color.darkerBlue
+        ])
+        .chartLegend(.hidden)
+        .frame(minHeight: geo.size.height * 0.12)
+        .frame(width: fullWidth(viewWidth: screenSize.width))
+        .chartXScale(domain: startMarker ... endMarker)
+        .chartXSelection(value: $selection)
+        .chartXAxis { basalChartXAxis }
+        .chartYAxis { cobIobChartYAxis }
+        .chartYScale(domain: combinedYDomain())
+    }
+
+    func combinedYDomain() -> ClosedRange<Double> {
+        let minValue = min(state.minValueCobChart, state.minValueIobChart)
+        let maxValue = max(state.maxValueCobChart, state.maxValueIobChart)
+        return Double(minValue) ... Double(maxValue)
+    }
+
+    private func drawSelectedInnerPoint(xValue: Date, yValue: Double, axis: String) -> some ChartContent {
+        PointMark(
+            x: .value("Time", xValue, unit: .minute),
+            y: .value("Value", yValue)
+        )
+        .symbolSize(CGSize(width: 6, height: 6))
+        .foregroundStyle(Color.primary)
+        .position(by: .value("Axis", axis))
+    }
+
+    private func drawSelectedOuterPoint(xValue: Date, yValue: Double, axis: String, color: Color) -> some ChartContent {
+        PointMark(
+            x: .value("Time", xValue, unit: .minute),
+            y: .value("Value", yValue)
+        )
+        .symbolSize(CGSize(width: 15, height: 15))
+        .foregroundStyle(color.opacity(0.8))
+        .position(by: .value("Axis", axis))
+    }
+
+    func drawCOBIOBChart() -> some ChartContent {
+        ForEach(state.enactedAndNonEnactedDeterminations) { item in
+
+            // MARK: - COB line and area mark
+
+            let amountCOB = Int(item.cob)
+            let date: Date = item.deliverAt ?? Date()
+
+            LineMark(x: .value("Time", date), y: .value("Value", amountCOB))
+                .foregroundStyle(by: .value("Type", "COB"))
+                .position(by: .value("Axis", "COB"))
+            AreaMark(x: .value("Time", date), y: .value("Value", amountCOB))
+                .foregroundStyle(by: .value("Type", "COB"))
+                .position(by: .value("Axis", "COB"))
+                .opacity(0.2)
+
+            // MARK: - IOB line and area mark
+
+            let rawAmount = item.iob?.doubleValue ?? 0
+
+            // as iob and cob share the same y axis and cob is usually >> iob we need to weigh iob visually
+            let amountIOB: Double = rawAmount > 0 ? rawAmount * 8 : rawAmount * 9
+
+            AreaMark(x: .value("Time", date), y: .value("Amount", amountIOB))
+                .foregroundStyle(by: .value("Type", "IOB"))
+                .position(by: .value("Axis", "IOB"))
+                .opacity(0.2)
+            LineMark(x: .value("Time", date), y: .value("Amount", amountIOB))
+                .foregroundStyle(by: .value("Type", "IOB"))
+                .position(by: .value("Axis", "IOB"))
+        }
+    }
+}

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

@@ -38,7 +38,9 @@ extension MainChartView {
             }
         }
         .id("DummyMainChart")
-        .frame(minHeight: geo.size.height * 0.28)
+        .frame(
+            minHeight: geo.size.height * (0.28 - safeAreaSize)
+        )
         .frame(width: screenSize.width - 10)
         .chartXAxis { mainChartXAxis }
         .chartXScale(domain: startMarker ... endMarker)
@@ -63,18 +65,16 @@ extension MainChartView {
     }
 
     var dummyCobChart: some View {
-        Chart {
-            drawCOB(dummy: true)
-        }
-        .id("DummyCobChart")
-        .frame(minHeight: geo.size.height * 0.12)
-        .frame(width: screenSize.width - 10)
-        .chartXScale(domain: startMarker ... endMarker)
-        .chartXAxis { basalChartXAxis }
-        .chartXAxis(.hidden)
-        .chartYAxis { cobChartYAxis }
-        .chartYAxis(.hidden)
-        .chartYScale(domain: state.minValueCobChart ... state.maxValueCobChart)
-        .chartLegend(.hidden)
+        Chart {}
+            .id("DummyCobChart")
+            .frame(minHeight: geo.size.height * 0.12)
+            .frame(width: screenSize.width - 10)
+            .chartXScale(domain: startMarker ... endMarker)
+            .chartXAxis { basalChartXAxis }
+            .chartXAxis(.hidden)
+            .chartYAxis { cobIobChartYAxis }
+            .chartYAxis(.hidden)
+            .chartYScale(domain: state.minValueCobChart ... state.maxValueCobChart)
+            .chartLegend(.hidden)
     }
 }

FreeAPS/Sources/Modules/Home/View/Chart/ForecastView.swift → FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/ForecastView.swift


FreeAPS/Sources/Modules/Home/View/Chart/GlucoseChartView.swift → FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift


+ 1 - 1
FreeAPS/Sources/Modules/Home/View/Chart/InsulinView.swift

@@ -32,7 +32,7 @@ struct InsulinView: ChartContent {
                     Image(systemName: "arrowtriangle.down.fill").font(.system(size: size)).foregroundStyle(Color.insulin)
                 }
                 .annotation(position: .top) {
-                    Text(MainChartHelper.bolusFormatter.string(from: amount) ?? "")
+                    Text(Formatter.bolusFormatter.string(from: amount) ?? "")
                         .font(.caption2)
                         .foregroundStyle(Color.primary)
                 }

+ 68 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/OverrideView.swift

@@ -0,0 +1,68 @@
+import Charts
+import CoreData
+import Foundation
+import SwiftUI
+
+struct OverrideView: ChartContent {
+    var state: Home.StateModel
+    let overrides: [OverrideStored]
+    let overrideRunStored: [OverrideRunStored]
+    let units: GlucoseUnits
+    let viewContext: NSManagedObjectContext
+
+    var body: some ChartContent {
+        drawActiveOverrides()
+        drawOverrideRunStored()
+    }
+
+    private func drawActiveOverrides() -> some ChartContent {
+        ForEach(overrides) { override in
+            let start: Date = override.date ?? .distantPast
+            let duration = MainChartHelper.calculateDuration(
+                objectID: override.objectID,
+                attribute: "duration",
+                context: viewContext
+            ) ?? 0
+            let end: Date = duration != 0 ? start.addingTimeInterval(duration) : start
+                .addingTimeInterval(6 * 60 * 60) // handle infinite overrides
+
+            let target = getOverrideTarget(override: override)
+
+            RuleMark(
+                xStart: .value("Start", start, unit: .second),
+                xEnd: .value("End", end, unit: .second),
+                y: .value("Value", units == .mgdL ? target : target.asMmolL)
+            )
+            .foregroundStyle(Color.purple.opacity(0.4))
+            .lineStyle(.init(lineWidth: 8))
+        }
+    }
+
+    private func drawOverrideRunStored() -> some ChartContent {
+        ForEach(overrideRunStored) { overrideRunStored in
+            let start: Date = overrideRunStored.startDate ?? .distantPast
+            let end: Date = overrideRunStored.endDate ?? Date()
+            let target = (overrideRunStored.target?.decimalValue ?? 100) == 0 ? 100 : overrideRunStored.target!.decimalValue
+            RuleMark(
+                xStart: .value("Start", start, unit: .second),
+                xEnd: .value("End", end, unit: .second),
+                y: .value("Value", units == .mgdL ? target : target.asMmolL)
+            )
+            .foregroundStyle(Color.purple.opacity(0.25))
+            .lineStyle(.init(lineWidth: 8))
+        }
+    }
+
+    // Handle Overrides where no Target is provided
+    private func getOverrideTarget(override: OverrideStored) -> Decimal {
+        if let target = MainChartHelper
+            .calculateTarget(objectID: override.objectID, attribute: "target", context: viewContext)
+        {
+            return target
+        } else if override.target == 0 {
+            return state.currentGlucoseTarget // Default target
+        } else {
+            return override.target?.decimalValue ?? state.currentGlucoseTarget
+        }
+    }
+}

+ 111 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/SelectionPopoverView.swift

@@ -0,0 +1,111 @@
+import Charts
+import Foundation
+import SwiftUICore
+
+struct SelectionPopoverView: ChartContent {
+    let selectedGlucose: GlucoseStored
+    let selectedIOBValue: OrefDetermination?
+    let selectedCOBValue: OrefDetermination?
+    let units: GlucoseUnits
+    let highGlucose: Decimal
+    let lowGlucose: Decimal
+    let currentGlucoseTarget: Decimal
+    let glucoseColorScheme: GlucoseColorScheme
+
+    private var glucoseToDisplay: Decimal {
+        units == .mgdL ? Decimal(selectedGlucose.glucose) : Decimal(selectedGlucose.glucose).asMmolL
+    }
+
+    private var pointMarkColor: Color {
+        let hardCodedLow = Decimal(55)
+        let hardCodedHigh = Decimal(220)
+        let isDynamicColorScheme = glucoseColorScheme == .dynamicColor
+
+        return FreeAPS.getDynamicGlucoseColor(
+            glucoseValue: Decimal(selectedGlucose.glucose),
+            highGlucoseColorValue: isDynamicColorScheme ? hardCodedHigh : highGlucose,
+            lowGlucoseColorValue: isDynamicColorScheme ? hardCodedLow : lowGlucose,
+            targetGlucose: currentGlucoseTarget,
+            glucoseColorScheme: glucoseColorScheme
+        )
+    }
+
+    var body: some ChartContent {
+        RuleMark(x: .value("Selection", selectedGlucose.date ?? Date.now, unit: .minute))
+            .foregroundStyle(Color.tabBar)
+            .offset(yStart: 70)
+            .lineStyle(.init(lineWidth: 2))
+            .annotation(
+                position: .top,
+                alignment: .center,
+                overflowResolution: .init(x: .fit(to: .chart), y: .fit(to: .chart))
+            ) {
+                selectionPopover
+            }
+
+        PointMark(
+            x: .value("Time", selectedGlucose.date ?? Date.now, unit: .minute),
+            y: .value("Value", glucoseToDisplay)
+        )
+        .zIndex(-1)
+        .symbolSize(CGSize(width: 15, height: 15))
+        .foregroundStyle(pointMarkColor)
+
+        PointMark(
+            x: .value("Time", selectedGlucose.date ?? Date.now, unit: .minute),
+            y: .value("Value", glucoseToDisplay)
+        )
+        .zIndex(-1)
+        .symbolSize(CGSize(width: 6, height: 6))
+        .foregroundStyle(Color.primary)
+    }
+
+    @ViewBuilder var selectionPopover: some View {
+        VStack(alignment: .leading) {
+            HStack {
+                Image(systemName: "clock")
+                Text(selectedGlucose.date?.formatted(.dateTime.hour().minute(.twoDigits)) ?? "")
+                    .font(.body).bold()
+            }
+            .font(.body).padding(.bottom, 5)
+
+            HStack {
+                Text(units == .mgdL ? glucoseToDisplay.description : glucoseToDisplay.formattedAsMmolL)
+                    .bold()
+                    + Text(" \(units.rawValue)")
+            }
+            .foregroundStyle(pointMarkColor)
+            .font(.body)
+
+            if let selectedIOBValue, let iob = selectedIOBValue.iob {
+                HStack {
+                    Image(systemName: "syringe.fill").frame(width: 15)
+                    Text(Formatter.bolusFormatter.string(from: iob) ?? "")
+                        .bold()
+                        + Text(NSLocalizedString(" U", comment: "Insulin unit"))
+                }
+                .foregroundStyle(Color.insulin).font(.body)
+            }
+
+            if let selectedCOBValue {
+                HStack {
+                    Image(systemName: "fork.knife").frame(width: 15)
+                    Text(Formatter.integerFormatter.string(from: selectedCOBValue.cob as NSNumber) ?? "")
+                        .bold()
+                        + Text(NSLocalizedString(" g", comment: "gram of carbs"))
+                }
+                .foregroundStyle(Color.orange).font(.body)
+            }
+        }
+        .padding()
+        .background {
+            RoundedRectangle(cornerRadius: 4)
+                .fill(Color.chart.opacity(0.85))
+                .shadow(color: Color.secondary, radius: 2)
+                .overlay(
+                    RoundedRectangle(cornerRadius: 4)
+                        .stroke(Color.secondary, lineWidth: 2)
+                )
+        }
+    }
+}

+ 0 - 0
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/TempTargets.swift


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini