Explorar el Código

Alternate Bolus Calculator and Edit of Meals

* Implement new Bolus Calculator, by @polscm32, in Swift.

* Refactor the new calculator above and the UI/UX of the new Bolus View

* Add  Feature: Edit the Meals from the Bolus View . Go from the  Carbs View <--> Bolus View .
Jon B Mårtensson hace 2 años
padre
commit
044f5d5a1b
Se han modificado 33 ficheros con 1274 adiciones y 433 borrados
  1. 9 1
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  2. 52 0
      FreeAPS.xcodeproj/project.pbxproj
  3. 5 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  4. 11 20
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  5. 2 2
      FreeAPS/Sources/Models/CarbsEntry.swift
  6. 20 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  7. 2 0
      FreeAPS/Sources/Models/NightscoutTreatment.swift
  8. 2 0
      FreeAPS/Sources/Models/Suggestion.swift
  9. 51 14
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  10. 7 2
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  11. 144 36
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  12. 23 0
      FreeAPS/Sources/Modules/Bolus/Components/CheckboxToggleStyle.swift
  13. 521 0
      FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift
  14. 8 261
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  15. 275 0
      FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift
  16. 5 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigDataFlow.swift
  17. 3 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigProvider.swift
  18. 27 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift
  19. 52 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  20. 1 1
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  21. 2 2
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  22. 1 1
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  23. 7 2
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  24. 2 0
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  25. 5 3
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  26. 1 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  27. 9 6
      FreeAPS/Sources/Router/Screen.swift
  28. 1 1
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  29. 6 5
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  30. 18 48
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  31. 1 1
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  32. 1 1
      FreeAPS/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  33. 0 25
      FreeAPS/Sources/Views/DecimalTextField.swift

+ 9 - 1
Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="22G90" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="22G120" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
     <entity name="BGaverages" representedClassName="BGaverages" syncable="YES" codeGenerationType="class">
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
         <attribute name="average_1" optional="YES" attributeType="Decimal" defaultValueString="0.0"/>
@@ -54,6 +54,14 @@
         <attribute name="loopStatus" optional="YES" attributeType="String"/>
         <attribute name="loopStatus" optional="YES" attributeType="String"/>
         <attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
         <attribute name="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
     </entity>
     </entity>
+    <entity name="Meals" representedClassName="Meals" syncable="YES" codeGenerationType="class">
+        <attribute name="carbs" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
+        <attribute name="fat" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+        <attribute name="id" optional="YES" attributeType="String"/>
+        <attribute name="note" optional="YES" attributeType="String"/>
+        <attribute name="protein" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
+    </entity>
     <entity name="Oref0Suggestion" representedClassName="Oref0Suggestion" syncable="YES" codeGenerationType="class">
     <entity name="Oref0Suggestion" representedClassName="Oref0Suggestion" syncable="YES" codeGenerationType="class">
         <relationship name="computedInsulinDistribution" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InsulinDistribution" inverseName="insulin" inverseEntity="InsulinDistribution"/>
         <relationship name="computedInsulinDistribution" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="InsulinDistribution" inverseName="insulin" inverseEntity="InsulinDistribution"/>
         <relationship name="computedTDD" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TDD" inverseName="computed" inverseEntity="TDD"/>
         <relationship name="computedTDD" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="TDD" inverseName="computed" inverseEntity="TDD"/>

+ 52 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -301,6 +301,13 @@
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C018D1680307A31C9ED7120 /* CGMStateModel.swift */; };
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500371C09F54F89A97D65FDB /* CalibrationsRootView.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
 		BD2B464E0745FBE7B79913F4 /* NightscoutConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */; };
+		BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.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 */; };
+		BDFD165A2AE40438007F0DDA /* AlternativeBolusCalcRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */; };
+		BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
@@ -819,6 +826,13 @@
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		B9CAAEFB2AE70836000F68BC /* branch.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = branch.txt; sourceTree = SOURCE_ROOT; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
 		BC210C0F3CB6D3C86E5DED4E /* LibreConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigRootView.swift; sourceTree = "<group>"; };
+		BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxToggleStyle.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>"; };
+		BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternativeBolusCalcRootView.swift; sourceTree = "<group>"; };
+		BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBolusCalcRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -1154,6 +1168,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */,
 				190EBCC229FF134900BA767D /* StatConfig */,
 				190EBCC229FF134900BA767D /* StatConfig */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				CE94597C29E9E1CD0047C9C6 /* WatchConfig */,
 				19F95FF129F10F9C00314DDC /* Stat */,
 				19F95FF129F10F9C00314DDC /* Stat */,
@@ -2039,6 +2054,35 @@
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
 				10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */,
 				10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */,
+				BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */,
+				BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		BD2FF19E2AE29D24005D1C5D /* Components */ = {
+			isa = PBXGroup;
+			children = (
+				BD2FF19F2AE29D43005D1C5D /* CheckboxToggleStyle.swift */,
+			);
+			path = Components;
+			sourceTree = "<group>";
+		};
+		BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */ = {
+			isa = PBXGroup;
+			children = (
+				BD7DA9A42AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift */,
+				BD7DA9A62AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift */,
+				BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */,
+				BD7DA9AA2AE06E9600601B20 /* View */,
+			);
+			path = BolusCalculatorConfig;
+			sourceTree = "<group>";
+		};
+		BD7DA9AA2AE06E9600601B20 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */,
 			);
 			);
 			path = View;
 			path = View;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -2057,6 +2101,7 @@
 		C2C98283C436DB934D7E7994 /* Bolus */ = {
 		C2C98283C436DB934D7E7994 /* Bolus */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				BD2FF19E2AE29D24005D1C5D /* Components */,
 				C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */,
 				C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */,
 				C19984D62EFC0035A9E9644D /* BolusProvider.swift */,
 				C19984D62EFC0035A9E9644D /* BolusProvider.swift */,
 				223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */,
 				223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */,
@@ -2632,6 +2677,7 @@
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DE6125C9D4D500A708ED /* ViewModifiers.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				3811DEAC25C9D88300A708ED /* NightscoutManager.swift in Sources */,
 				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
 				19A910302A24BF6300C8951B /* StatsView.swift in Sources */,
+				BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				CEB434E528B8FF5D00B70274 /* UIColor.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */,
 				190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
@@ -2663,6 +2709,7 @@
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
+				BDFD165A2AE40438007F0DDA /* AlternativeBolusCalcRootView.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				190EBCC429FF136900BA767D /* StatConfigDataFlow.swift in Sources */,
 				190EBCC429FF136900BA767D /* StatConfigDataFlow.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
@@ -2688,6 +2735,7 @@
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				9825E5E923F0B8FA80C8C7C7 /* NightscoutConfigStateModel.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				38A43598262E0E4900E80935 /* FetchAnnouncementsManager.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
 				642F76A05A4FF530463A9FD0 /* NightscoutConfigRootView.swift in Sources */,
+				BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				AD3D2CD42CD01B9EB8F26522 /* PumpConfigDataFlow.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
 				5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */,
@@ -2729,6 +2777,7 @@
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
+				BD7DA9A72AE06E2B00601B20 /* BolusCalculatorConfigProvider.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				FEFA5C0F299F810B00765C17 /* Core_Data.xcdatamodeld in Sources */,
 				FEFA5C0F299F810B00765C17 /* Core_Data.xcdatamodeld in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
@@ -2768,6 +2817,7 @@
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				CE82E02728E869DF00473A9C /* AlertEntry.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
+				BD2FF1A02AE29D43005D1C5D /* CheckboxToggleStyle.swift in Sources */,
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
@@ -2780,6 +2830,7 @@
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
+				BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
@@ -2807,6 +2858,7 @@
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
 				F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */,
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
 				BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */,
 				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
 				61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */,
+				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
 				6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
 				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,
 				903D18976088B09110BCBE29 /* LibreConfigStateModel.swift in Sources */,

+ 5 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -42,5 +42,9 @@
   "oneDimensionalGraph" : false,
   "oneDimensionalGraph" : false,
   "rulerMarks" : false,
   "rulerMarks" : false,
   "maxCarbs": 1000,
   "maxCarbs": 1000,
-  "displayFatAndProteinOnWatch": false
+  "displayFatAndProteinOnWatch": false,
+  "overrideFactor": 0.8,
+  "useCalc": false,
+  "fattyMeals": false,
+  "fattyMealFactor": 0.7
 }
 }

+ 11 - 20
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -12,7 +12,7 @@ protocol CarbsStorage {
     func syncDate() -> Date
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func recent() -> [CarbsEntry]
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
-    func deleteCarbs(at date: Date)
+    func deleteCarbs(at uniqueID: String)
 }
 }
 
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -71,7 +71,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 // New date for each carb equivalent
                 // New date for each carb equivalent
                 var useDate = entries.last?.createdAt ?? Date()
                 var useDate = entries.last?.createdAt ?? Date()
                 // Group and Identify all FPUs together
                 // Group and Identify all FPUs together
-                let fpuID = UUID().uuidString
+                let fpuID = (entries.last?.collectionID ?? "") + ".fpu"
                 // Create an array of all future carb equivalents.
                 // Create an array of all future carb equivalents.
                 var futureCarbArray = [CarbsEntry]()
                 var futureCarbArray = [CarbsEntry]()
                 while carbEquivalents > 0, numberOfEquivalents > 0 {
                 while carbEquivalents > 0, numberOfEquivalents > 0 {
@@ -81,7 +81,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
                     } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
 
 
                     let eachCarbEntry = CarbsEntry(
                     let eachCarbEntry = CarbsEntry(
-                        id: UUID().uuidString, createdAt: useDate, carbs: equivalent, fat: 0, protein: 0, note: nil,
+                        collectionID: fpuID, createdAt: useDate, carbs: equivalent, fat: 0, protein: 0, note: nil,
                         enteredBy: CarbsEntry.manual, isFPU: true,
                         enteredBy: CarbsEntry.manual, isFPU: true,
                         fpuID: fpuID
                         fpuID: fpuID
                     )
                     )
@@ -101,7 +101,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             } // ------------------------- END OF TPU ----------------------------------------
             } // ------------------------- END OF TPU ----------------------------------------
             // Store the actual (normal) carbs
             // Store the actual (normal) carbs
             if entries.last?.carbs ?? 0 > 0 {
             if entries.last?.carbs ?? 0 > 0 {
-                uniqEvents = []
+                // uniqEvents = []
                 self.storage.transaction { storage in
                 self.storage.transaction { storage in
                     storage.append(entries, to: file, uniqBy: \.createdAt)
                     storage.append(entries, to: file, uniqBy: \.createdAt)
                     uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
                     uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
@@ -143,24 +143,14 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
         storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self)?.reversed() ?? []
     }
     }
 
 
-    func deleteCarbs(at date: Date) {
+    func deleteCarbs(at uniqueID: String) {
         processQueue.sync {
         processQueue.sync {
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
             var allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
 
 
-            guard let entryIndex = allValues.firstIndex(where: { $0.createdAt == date }) else {
-                return
-            }
-
-            // If deleteing a FPUs remove all of those with the same ID
-            if allValues[entryIndex].isFPU != nil, allValues[entryIndex].isFPU ?? false {
-                let fpuString = allValues[entryIndex].fpuID
-                allValues.removeAll(where: { $0.fpuID == fpuString })
-                storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
-                broadcaster.notify(CarbsObserver.self, on: processQueue) {
-                    $0.carbsDidUpdate(allValues)
-                }
+            if allValues.firstIndex(where: { $0.collectionID == uniqueID }) == nil {
+                debug(.default, "Didn't find any carb entries to delete. ID to search for: " + uniqueID.description)
             } else {
             } else {
-                allValues.remove(at: entryIndex)
+                allValues.removeAll(where: { $0.collectionID == uniqueID })
                 storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
                 storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
                 broadcaster.notify(CarbsObserver.self, on: processQueue) {
                 broadcaster.notify(CarbsObserver.self, on: processQueue) {
                     $0.carbsDidUpdate(allValues)
                     $0.carbsDidUpdate(allValues)
@@ -170,7 +160,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
     }
 
 
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
-        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedPumphistory, as: [NigtscoutTreatment].self) ?? []
+        let uploaded = storage.retrieve(OpenAPS.Nightscout.uploadedCarbs, as: [NigtscoutTreatment].self) ?? []
 
 
         let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
         let eventsManual = recent().filter { $0.enteredBy == CarbsEntry.manual }
         let treatments = eventsManual.map {
         let treatments = eventsManual.map {
@@ -190,7 +180,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 protein: nil,
                 protein: nil,
                 foodType: $0.note,
                 foodType: $0.note,
                 targetTop: nil,
                 targetTop: nil,
-                targetBottom: nil
+                targetBottom: nil,
+                collectionID: $0.collectionID
             )
             )
         }
         }
         return Array(Set(treatments).subtracting(Set(uploaded)))
         return Array(Set(treatments).subtracting(Set(uploaded)))

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

@@ -1,7 +1,7 @@
 import Foundation
 import Foundation
 
 
 struct CarbsEntry: JSON, Equatable, Hashable {
 struct CarbsEntry: JSON, Equatable, Hashable {
-    let id: String?
+    let collectionID: String?
     let createdAt: Date
     let createdAt: Date
     let carbs: Decimal
     let carbs: Decimal
     let fat: Decimal?
     let fat: Decimal?
@@ -25,7 +25,7 @@ struct CarbsEntry: JSON, Equatable, Hashable {
 
 
 extension CarbsEntry {
 extension CarbsEntry {
     private enum CodingKeys: String, CodingKey {
     private enum CodingKeys: String, CodingKey {
-        case id = "_id"
+        case collectionID = "_id"
         case createdAt = "created_at"
         case createdAt = "created_at"
         case carbs
         case carbs
         case fat
         case fat

+ 20 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -45,6 +45,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var maxCarbs: Decimal = 1000
     var maxCarbs: Decimal = 1000
     var displayFatAndProteinOnWatch: Bool = false
     var displayFatAndProteinOnWatch: Bool = false
     var onlyAutotuneBasals: Bool = false
     var onlyAutotuneBasals: Bool = false
+    var overrideFactor: Decimal = 0.8
+    var useCalc: Bool = false
+    var fattyMeals: Bool = false
+    var fattyMealFactor: Decimal = 0.7
 }
 }
 
 
 extension FreeAPSSettings: Decodable {
 extension FreeAPSSettings: Decodable {
@@ -139,6 +143,22 @@ extension FreeAPSSettings: Decodable {
             settings.individualAdjustmentFactor = individualAdjustmentFactor
             settings.individualAdjustmentFactor = individualAdjustmentFactor
         }
         }
 
 
+        if let useCalc = try? container.decode(Bool.self, forKey: .useCalc) {
+            settings.useCalc = useCalc
+        }
+
+        if let fattyMeals = try? container.decode(Bool.self, forKey: .fattyMeals) {
+            settings.fattyMeals = fattyMeals
+        }
+
+        if let fattyMealFactor = try? container.decode(Decimal.self, forKey: .fattyMealFactor) {
+            settings.fattyMealFactor = fattyMealFactor
+        }
+
+        if let overrideFactor = try? container.decode(Decimal.self, forKey: .overrideFactor) {
+            settings.overrideFactor = overrideFactor
+        }
+
         if let timeCap = try? container.decode(Int.self, forKey: .timeCap) {
         if let timeCap = try? container.decode(Int.self, forKey: .timeCap) {
             settings.timeCap = timeCap
             settings.timeCap = timeCap
         }
         }

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

@@ -21,6 +21,7 @@ struct NigtscoutTreatment: JSON, Hashable, Equatable {
     var glucoseType: String?
     var glucoseType: String?
     var glucose: String?
     var glucose: String?
     var units: String?
     var units: String?
+    var collectionID: String?
 
 
     static let local = "iAPS"
     static let local = "iAPS"
 
 
@@ -57,5 +58,6 @@ extension NigtscoutTreatment {
         case glucoseType
         case glucoseType
         case glucose
         case glucose
         case units
         case units
+        case collectionID
     }
     }
 }
 }

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

@@ -29,6 +29,7 @@ struct Suggestion: JSON, Equatable {
     let minGuardBG: Decimal?
     let minGuardBG: Decimal?
     let minPredBG: Decimal?
     let minPredBG: Decimal?
     let threshold: Decimal?
     let threshold: Decimal?
+    let carbRatio: Decimal?
 }
 }
 
 
 struct Predictions: JSON, Equatable {
 struct Predictions: JSON, Equatable {
@@ -75,6 +76,7 @@ extension Suggestion {
         case minGuardBG
         case minGuardBG
         case minPredBG
         case minPredBG
         case threshold
         case threshold
+        case carbRatio = "CR"
     }
     }
 }
 }
 
 

+ 51 - 14
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -18,6 +18,8 @@ extension AddCarbs {
         @Published var summation: [String] = []
         @Published var summation: [String] = []
         @Published var maxCarbs: Decimal = 0
         @Published var maxCarbs: Decimal = 0
         @Published var note: String = ""
         @Published var note: String = ""
+        @Published var id_: String = ""
+        @Published var summary: String = ""
 
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
 
@@ -33,25 +35,28 @@ extension AddCarbs {
                 return
                 return
             }
             }
             carbs = min(carbs, maxCarbs)
             carbs = min(carbs, maxCarbs)
-
-            carbsStorage.storeCarbs(
-                [CarbsEntry(
-                    id: UUID().uuidString,
-                    createdAt: date,
-                    carbs: carbs,
-                    fat: fat,
-                    protein: protein,
-                    note: note,
-                    enteredBy: CarbsEntry.manual,
-                    isFPU: false, fpuID: nil
-                )]
-            )
+            id_ = UUID().uuidString
+
+            let carbsToStore = [CarbsEntry(
+                collectionID: id_,
+                createdAt: date,
+                carbs: carbs,
+                fat: fat,
+                protein: protein,
+                note: note,
+                enteredBy: CarbsEntry.manual,
+                isFPU: false, fpuID: nil
+            )]
+            carbsStorage.storeCarbs(carbsToStore)
 
 
             if settingsManager.settings.skipBolusScreenAfterCarbs {
             if settingsManager.settings.skipBolusScreenAfterCarbs {
                 apsManager.determineBasalSync()
                 apsManager.determineBasalSync()
                 showModal(for: nil)
                 showModal(for: nil)
+            } else if carbs > 0 {
+                saveToCoreData(carbsToStore)
+                showModal(for: .bolus(waitForSuggestion: true, fetch: true))
             } else {
             } else {
-                showModal(for: .bolus(waitForSuggestion: true))
+                hideModal()
             }
             }
         }
         }
 
 
@@ -160,5 +165,37 @@ extension AddCarbs {
             }
             }
             return waitersNotepadString
             return waitersNotepadString
         }
         }
+
+        func loadEntries(_ editMode: Bool) {
+            if editMode {
+                coredataContext.perform {
+                    var mealToEdit = [Meals]()
+                    let requestMeal = Meals.fetchRequest() as NSFetchRequest<Meals>
+                    let sortMeal = NSSortDescriptor(key: "createdAt", ascending: false)
+                    requestMeal.sortDescriptors = [sortMeal]
+                    requestMeal.fetchLimit = 1
+                    try? mealToEdit = self.coredataContext.fetch(requestMeal)
+
+                    self.carbs = Decimal(mealToEdit.first?.carbs ?? 0)
+                    self.fat = Decimal(mealToEdit.first?.fat ?? 0)
+                    self.protein = Decimal(mealToEdit.first?.protein ?? 0)
+                    self.note = mealToEdit.first?.note ?? ""
+                    self.id_ = mealToEdit.first?.id ?? ""
+                }
+            }
+        }
+
+        func saveToCoreData(_ stored: [CarbsEntry]) {
+            let save = Meals(context: coredataContext)
+            save.createdAt = stored.first?.createdAt ?? .distantPast
+            save.id = stored.first?.collectionID ?? ""
+            save.carbs = Double(stored.first?.carbs ?? 0)
+            save.fat = Double(stored.first?.fat ?? 0)
+            save.protein = Double(stored.first?.protein ?? 0)
+            save.note = stored.first?.note ?? ""
+            if coredataContext.hasChanges {
+                try? coredataContext.save()
+            }
+        }
     }
     }
 }
 }

+ 7 - 2
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -5,6 +5,7 @@ import Swinject
 extension AddCarbs {
 extension AddCarbs {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
+        let editMode: Bool
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
         @State var dish: String = ""
         @State var dish: String = ""
         @State var isPromptPresented = false
         @State var isPromptPresented = false
@@ -118,7 +119,7 @@ extension AddCarbs {
 
 
                 Section {
                 Section {
                     Button { state.add() }
                     Button { state.add() }
-                    label: { Text("Save and continue") }
+                    label: { Text(state.carbs > 0 ? "Save and continue" : "Save") }
                         .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                         .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                         .frame(maxWidth: .infinity, alignment: .center)
                         .frame(maxWidth: .infinity, alignment: .center)
                 } footer: { Text(state.waitersNotepad().description) }
                 } footer: { Text(state.waitersNotepad().description) }
@@ -129,7 +130,11 @@ extension AddCarbs {
                     }
                     }
                 }
                 }
             }
             }
-            .onAppear(perform: configureView)
+            .onAppear {
+                configureView {
+                    state.loadEntries(editMode)
+                }
+            }
             .navigationTitle("Add Meals")
             .navigationTitle("Add Meals")
             .navigationBarTitleDisplayMode(.inline)
             .navigationBarTitleDisplayMode(.inline)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
             .navigationBarItems(leading: Button("Close", action: state.hideModal))

+ 144 - 36
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -1,3 +1,5 @@
+
+import LoopKit
 import SwiftUI
 import SwiftUI
 import Swinject
 import Swinject
 
 
@@ -7,28 +9,63 @@ extension Bolus {
         @Injected() var apsManager: APSManager!
         @Injected() var apsManager: APSManager!
         @Injected() var broadcaster: Broadcaster!
         @Injected() var broadcaster: Broadcaster!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
+        // added for bolus calculator
+        @Injected() var glucoseStorage: GlucoseStorage!
+        @Injected() var settings: SettingsManager!
+        @Injected() var nsManager: NightscoutManager!
 
 
+        @Published var suggestion: Suggestion?
         @Published var amount: Decimal = 0
         @Published var amount: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRequired: Decimal = 0
         @Published var insulinRequired: Decimal = 0
-        @Published var waitForSuggestion: Bool = false
-        @Published var error: Bool = false
+        @Published var units: GlucoseUnits = .mmolL
+        @Published var percentage: Decimal = 0
+        @Published var threshold: Decimal = 0
+        @Published var maxBolus: Decimal = 0
         @Published var errorString: Decimal = 0
         @Published var errorString: Decimal = 0
         @Published var evBG: Int = 0
         @Published var evBG: Int = 0
         @Published var insulin: Decimal = 0
         @Published var insulin: Decimal = 0
-        @Published var target: Decimal = 0
         @Published var isf: Decimal = 0
         @Published var isf: Decimal = 0
-        @Published var percentage: Decimal = 0
-        @Published var threshold: Decimal = 0
+        @Published var error: Bool = false
         @Published var minGuardBG: Decimal = 0
         @Published var minGuardBG: Decimal = 0
         @Published var minDelta: Decimal = 0
         @Published var minDelta: Decimal = 0
         @Published var expectedDelta: Decimal = 0
         @Published var expectedDelta: Decimal = 0
         @Published var minPredBG: Decimal = 0
         @Published var minPredBG: Decimal = 0
-        @Published var units: GlucoseUnits = .mmolL
-        @Published var maxBolus: Decimal = 0
+        @Published var waitForSuggestion: Bool = false
+        @Published var carbRatio: Decimal = 0
 
 
         var waitForSuggestionInitial: Bool = false
         var waitForSuggestionInitial: Bool = false
 
 
+        // added for bolus calculator
+        @Published var recentGlucose: BloodGlucose?
+        @Published var target: Decimal = 0
+        @Published var cob: Decimal = 0
+        @Published var iob: Decimal = 0
+
+        @Published var currentBG: Decimal = 0
+        @Published var fifteenMinInsulin: Decimal = 0
+        @Published var deltaBG: Decimal = 0
+        @Published var targetDifferenceInsulin: Decimal = 0
+        @Published var wholeCobInsulin: Decimal = 0
+        @Published var iobInsulinReduction: Decimal = 0
+        @Published var wholeCalc: Decimal = 0
+        @Published var roundedWholeCalc: Decimal = 0
+        @Published var insulinCalculated: Decimal = 0
+        @Published var roundedInsulinCalculated: Decimal = 0
+        @Published var fraction: Decimal = 0
+        @Published var useCalc: Bool = false
+        @Published var basal: Decimal = 0
+        @Published var fattyMeals: Bool = false
+        @Published var fattyMealFactor: Decimal = 0
+        @Published var useFattyMealCorrectionFactor: Bool = false
+        @Published var eventualBG: Int = 0
+
+        @Published var meal: [CarbsEntry]?
+        @Published var carbs: Decimal = 0
+        @Published var fat: Decimal = 0
+        @Published var protein: Decimal = 0
+        @Published var note: String = ""
+
         override func subscribe() {
         override func subscribe() {
             setupInsulinRequired()
             setupInsulinRequired()
             broadcaster.register(SuggestionObserver.self, observer: self)
             broadcaster.register(SuggestionObserver.self, observer: self)
@@ -36,6 +73,11 @@ extension Bolus {
             percentage = settingsManager.settings.insulinReqPercentage
             percentage = settingsManager.settings.insulinReqPercentage
             threshold = provider.suggestion?.threshold ?? 0
             threshold = provider.suggestion?.threshold ?? 0
             maxBolus = provider.pumpSettings().maxBolus
             maxBolus = provider.pumpSettings().maxBolus
+            // added
+            fraction = settings.settings.overrideFactor
+            useCalc = settings.settings.useCalc
+            fattyMeals = settings.settings.fattyMeals
+            fattyMealFactor = settings.settings.fattyMealFactor
 
 
             if waitForSuggestionInitial {
             if waitForSuggestionInitial {
                 apsManager.determineBasal()
                 apsManager.determineBasal()
@@ -51,13 +93,79 @@ extension Bolus {
             }
             }
         }
         }
 
 
+        func getDeltaBG() {
+            let glucose = glucoseStorage.recent()
+            guard glucose.count >= 3 else { return }
+            let lastGlucose = glucose.last!
+            let thirdLastGlucose = glucose[glucose.count - 3]
+            let delta = Decimal(lastGlucose.glucose!) - Decimal(thirdLastGlucose.glucose!)
+            deltaBG = delta
+        }
+
+        // CALCULATIONS FOR THE BOLUS CALCULATOR
+        func calculateInsulin() -> Decimal {
+            // for mmol conversion
+            var conversion: Decimal = 1.0
+            if units == .mmolL {
+                conversion = 0.0555
+            }
+            // insulin needed for the current blood glucose
+            let targetDifference = (currentBG - target) * conversion
+            targetDifferenceInsulin = targetDifference / isf
+
+            // more or less insulin because of bg trend in the last 15 minutes
+            fifteenMinInsulin = (deltaBG * conversion) / isf
+
+            // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB
+            wholeCobInsulin = cob / carbRatio
+
+            // determine how much the calculator reduces/ increases the bolus because of IOB
+            iobInsulinReduction = (-1) * iob
+
+            // adding everything together
+            // add a calc for the case that no fifteenMinInsulin is available
+            if deltaBG != 0 {
+                wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin)
+            } else {
+                // add (rare) case that no glucose value is available -> maybe display warning?
+                // if no bg is available, ?? sets its value to 0
+                if currentBG == 0 {
+                    wholeCalc = (iobInsulinReduction + wholeCobInsulin)
+                } else {
+                    wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin)
+                }
+            }
+            // rounding
+            let wholeCalcAsDouble = Double(wholeCalc)
+            roundedWholeCalc = Decimal(round(100 * wholeCalcAsDouble) / 100)
+
+            // apply custom factor at the end of the calculations
+            let result = wholeCalc * fraction
+
+            // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView)
+            if useFattyMealCorrectionFactor {
+                insulinCalculated = result * fattyMealFactor
+            } else {
+                insulinCalculated = result
+            }
+
+            // display no negative insulinCalculated
+            insulinCalculated = max(insulinCalculated, 0)
+            let insulinCalculatedAsDouble = Double(insulinCalculated)
+            roundedInsulinCalculated = Decimal(round(100 * insulinCalculatedAsDouble) / 100)
+
+            insulinCalculated = min(insulinCalculated, maxBolus)
+
+            return insulinCalculated
+        }
+
         func add() {
         func add() {
             guard amount > 0 else {
             guard amount > 0 else {
                 showModal(for: nil)
                 showModal(for: nil)
                 return
                 return
             }
             }
 
 
-            let maxAmount = Double(min(amount, maxBolus))
+            let maxAmount = Double(min(amount, provider.pumpSettings().maxBolus))
 
 
             unlockmanager.unlock()
             unlockmanager.unlock()
                 .sink { _ in } receiveValue: { [weak self] _ in
                 .sink { _ in } receiveValue: { [weak self] _ in
@@ -68,38 +176,10 @@ extension Bolus {
                 .store(in: &lifetime)
                 .store(in: &lifetime)
         }
         }
 
 
-        func addWithoutBolus() {
-            guard amount > 0 else {
-                showModal(for: nil)
-                return
-            }
-            amount = min(amount, maxBolus * 3) // Allow for 3 * Max Bolus for non-pump insulin
-
-            pumpHistoryStorage.storeEvents(
-                [
-                    PumpHistoryEvent(
-                        id: UUID().uuidString,
-                        type: .bolus,
-                        timestamp: Date(),
-                        amount: amount,
-                        duration: nil,
-                        durationMin: nil,
-                        rate: nil,
-                        temp: nil,
-                        carbInput: nil,
-                        isExternal: true
-                    )
-                ]
-            )
-            showModal(for: nil)
-        }
-
         func setupInsulinRequired() {
         func setupInsulinRequired() {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
                 self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
                 self.insulinRequired = self.provider.suggestion?.insulinReq ?? 0
 
 
-                // Manual Bolus recommendation (normally) yields a higher amount than the insulin reqiured amount computed for SMBs (auto boluses). A manual bolus threfore now (test) uses the Eventual BG for glucose prediction, whereas the insulinReg for SMBs uses the minPredBG for glucose prediction (typically lower than Eventual BG).
-
                 var conversion: Decimal = 1.0
                 var conversion: Decimal = 1.0
                 if self.units == .mmolL {
                 if self.units == .mmolL {
                     conversion = 0.0555
                     conversion = 0.0555
@@ -109,6 +189,11 @@ extension Bolus {
                 self.insulin = self.provider.suggestion?.insulinForManualBolus ?? 0
                 self.insulin = self.provider.suggestion?.insulinForManualBolus ?? 0
                 self.target = self.provider.suggestion?.current_target ?? 0
                 self.target = self.provider.suggestion?.current_target ?? 0
                 self.isf = self.provider.suggestion?.isf ?? 0
                 self.isf = self.provider.suggestion?.isf ?? 0
+                self.iob = self.provider.suggestion?.iob ?? 0
+                self.currentBG = (self.provider.suggestion?.bg ?? 0)
+                self.cob = self.provider.suggestion?.cob ?? 0
+                self.basal = self.provider.suggestion?.rate ?? 0
+                self.carbRatio = self.provider.suggestion?.carbRatio ?? 0
 
 
                 if self.settingsManager.settings.insulinReqPercentage != 100 {
                 if self.settingsManager.settings.insulinReqPercentage != 100 {
                     self.insulinRecommended = self.insulin * (self.settingsManager.settings.insulinReqPercentage / 100)
                     self.insulinRecommended = self.insulin * (self.settingsManager.settings.insulinReqPercentage / 100)
@@ -125,7 +210,30 @@ extension Bolus {
 
 
                 self.insulinRecommended = self.apsManager
                 self.insulinRecommended = self.apsManager
                     .roundBolus(amount: max(self.insulinRecommended, 0))
                     .roundBolus(amount: max(self.insulinRecommended, 0))
+
+                if self.useCalc {
+                    self.getDeltaBG()
+                    self.insulinCalculated = self.calculateInsulin()
+                }
+            }
+        }
+
+        func backToCarbsView(complexEntry: Bool, _ id: String) {
+            if complexEntry {
+                DispatchQueue.safeMainSync {
+                    nsManager.deleteCarbs(
+                        at: id, isFPU: nil, fpuID: nil, syncID: id
+                    )
+                    nsManager.deleteCarbs(
+                        at: id + ".fpu", isFPU: nil, fpuID: nil, syncID: id
+                    )
+                }
+            } else {
+                nsManager.deleteCarbs(
+                    at: id, isFPU: nil, fpuID: nil, syncID: id
+                )
             }
             }
+            showModal(for: .addCarbs(editMode: complexEntry))
         }
         }
     }
     }
 }
 }

+ 23 - 0
FreeAPS/Sources/Modules/Bolus/Components/CheckboxToggleStyle.swift

@@ -0,0 +1,23 @@
+import SwiftUI
+
+struct CheckboxToggleStyle: ToggleStyle {
+    func makeBody(configuration: Self.Configuration) -> some View {
+        HStack {
+            RoundedRectangle(cornerRadius: 5)
+                .stroke(lineWidth: 2)
+                .frame(width: 20, height: 20)
+                .cornerRadius(5)
+                .overlay {
+                    if configuration.isOn {
+                        Image(systemName: "checkmark")
+                    }
+                }
+                .onTapGesture {
+                    withAnimation {
+                        configuration.isOn.toggle()
+                    }
+                }
+            configuration.label
+        }
+    }
+}

+ 521 - 0
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift

@@ -0,0 +1,521 @@
+import Charts
+import CoreData
+import SwiftUI
+import Swinject
+
+extension Bolus {
+    struct AlternativeBolusCalcRootView: BaseView {
+        let resolver: Resolver
+        let waitForSuggestion: Bool
+        let fetch: Bool
+        @StateObject var state: StateModel
+        @State private var showInfo = false
+        @State private var exceededMaxBolus = false
+
+        private enum Config {
+            static let dividerHeight: CGFloat = 2
+            static let overlayColour: Color = .white // Currently commented out
+            static let spacing: CGFloat = 3
+        }
+
+        @Environment(\.colorScheme) var colorScheme
+
+        @FetchRequest(
+            entity: Meals.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "createdAt", ascending: false)]
+        ) var meal: FetchedResults<Meals>
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        private var mealFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+            return formatter
+        }
+
+        private var gluoseFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            if state.units == .mmolL {
+                formatter.maximumFractionDigits = 1
+            } else { formatter.maximumFractionDigits = 0 }
+            return formatter
+        }
+
+        private var fractionDigits: Int {
+            if state.units == .mmolL {
+                return 1
+            } else { return 0 }
+        }
+
+        var body: some View {
+            Form {
+                if state.waitForSuggestion {
+                    HStack {
+                        Text("Wait please").foregroundColor(.secondary)
+                        Spacer()
+                        ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
+                    }
+                }
+                Section {
+                    if fetch {
+                        VStack {
+                            if let carbs = meal.first?.carbs, carbs > 0 {
+                                HStack {
+                                    Text("Carbs")
+                                    Spacer()
+                                    Text(carbs.formatted())
+                                    Text("g")
+                                }.foregroundColor(.secondary)
+                            }
+                            if let fat = meal.first?.fat, fat > 0 {
+                                HStack {
+                                    Text("Fat")
+                                    Spacer()
+                                    Text(fat.formatted())
+                                    Text("g")
+                                }.foregroundColor(.secondary)
+                            }
+                            if let protein = meal.first?.protein, protein > 0 {
+                                HStack {
+                                    Text("Protein")
+                                    Spacer()
+                                    Text(protein.formatted())
+                                    Text("g")
+                                }.foregroundColor(.secondary)
+                            }
+                            if let note = meal.first?.note, note != "" {
+                                HStack {
+                                    Text("Note")
+                                    Spacer()
+                                    Text(note)
+                                }.foregroundColor(.secondary)
+                            }
+                        }
+                    } else {
+                        Text("No Meal")
+                    }
+                } header: { Text("Meal Summary") }
+
+                Section {
+                    Button {
+                        let id_ = meal.first?.id ?? ""
+                        state.backToCarbsView(complexEntry: fetch, id_)
+                    }
+                    label: { Text("Edit Meal / Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
+                }
+
+                Section {
+                    HStack {
+                        Button(action: {
+                            showInfo.toggle()
+                        }, label: {
+                            Image(systemName: "info.circle")
+                            Text("Calculations")
+                        })
+                            .foregroundStyle(.blue)
+                            .font(.footnote)
+                            .buttonStyle(PlainButtonStyle())
+                            .frame(maxWidth: .infinity, alignment: .leading)
+                        if state.fattyMeals {
+                            Spacer()
+                            Toggle(isOn: $state.useFattyMealCorrectionFactor) {
+                                Text("Fatty Meal")
+                            }
+                            .toggleStyle(CheckboxToggleStyle())
+                            .font(.footnote)
+                            .onChange(of: state.useFattyMealCorrectionFactor) { _ in
+                                state.insulinCalculated = state.calculateInsulin()
+                            }
+                        }
+                    }
+
+                    if state.waitForSuggestion {
+                        HStack {
+                            Text("Wait please").foregroundColor(.secondary)
+                            Spacer()
+                            ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
+                        }
+                    } else {
+                        HStack {
+                            Text("Recommended Bolus")
+                            Spacer()
+                            Text(
+                                formatter
+                                    .string(from: Double(state.insulinCalculated) as NSNumber) ?? ""
+                            )
+                            Text(
+                                NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
+                            ).foregroundColor(.secondary)
+                        }.contentShape(Rectangle())
+                            .onTapGesture { state.amount = state.insulinCalculated }
+                    }
+
+                    if !state.waitForSuggestion {
+                        HStack {
+                            Text("Bolus")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.amount,
+                                formatter: formatter,
+                                autofocus: false,
+                                cleanInput: true
+                            )
+                            Text(exceededMaxBolus ? "😵" : " U").foregroundColor(.secondary)
+                        }
+                        .onChange(of: state.amount) { newValue in
+                            if newValue > state.maxBolus {
+                                exceededMaxBolus = true
+                            } else {
+                                exceededMaxBolus = false
+                            }
+                        }
+                    }
+                } header: { Text("Bolus Summary") }
+
+                Section {
+                    if state.amount == 0, waitForSuggestion {
+                        Button { state.showModal(for: nil) }
+                        label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
+                    } else {
+                        Button { state.add() }
+                        label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .foregroundColor(exceededMaxBolus ? .loopRed : .accentColor)
+                            .disabled(
+                                state.amount <= 0 || state.amount > state.maxBolus
+                            )
+                    }
+                }
+            }
+            .blur(radius: showInfo ? 3 : 0)
+            .navigationTitle("Enact Bolus")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+
+            .onAppear {
+                configureView {
+                    state.waitForSuggestionInitial = waitForSuggestion
+                    state.waitForSuggestion = waitForSuggestion
+                    state.insulinCalculated = state.calculateInsulin()
+                }
+            }
+
+            .popup(isPresented: showInfo) {
+                bolusInfoAlternativeCalculator
+            }
+        }
+
+        var changed: Bool {
+            ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
+        }
+
+        var hasFatOrProtein: Bool {
+            ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
+        }
+
+        // Pop-up
+        var bolusInfoAlternativeCalculator: some View {
+            VStack {
+                let unit = NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
+                VStack {
+                    VStack(spacing: Config.spacing) {
+                        HStack {
+                            Text("Calculations")
+                                .font(.title3).frame(maxWidth: .infinity, alignment: .center)
+                        }.padding(10)
+
+                        if fetch {
+                            VStack {
+                                if let note = meal.first?.note, note != "" {
+                                    HStack {
+                                        Text("Note")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(note)
+                                    }
+                                }
+                                if let carbs = meal.first?.carbs, carbs > 0 {
+                                    HStack {
+                                        Text("Carbs")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(mealFormatter.string(from: carbs as NSNumber) ?? "")
+                                        Text("g").foregroundColor(.secondary)
+                                    }
+                                }
+                                if let protein = meal.first?.protein, protein > 0 {
+                                    HStack {
+                                        Text("Protein")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(mealFormatter.string(from: protein as NSNumber) ?? "")
+                                        Text("g").foregroundColor(.secondary)
+                                    }
+                                }
+                                if let fat = meal.first?.fat, fat > 0 {
+                                    HStack {
+                                        Text("Fat")
+                                            .foregroundColor(.secondary)
+                                        Spacer()
+                                        Text(mealFormatter.string(from: fat as NSNumber) ?? "")
+                                        Text("g").foregroundColor(.secondary)
+                                    }
+                                }
+                            }.padding()
+                        }
+
+                        if fetch { Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+                        }
+
+                        VStack {
+                            HStack {
+                                Text("Carb Ratio")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                Text(state.carbRatio.formatted())
+                                Text(NSLocalizedString(" g/U", comment: " grams per Unit"))
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("ISF")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let isf = state.isf
+                                Text(isf.formatted())
+                                Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Target Glucose")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let target = state.units == .mmolL ? state.target.asMmolL : state.target
+                                Text(
+                                    target
+                                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+                                )
+                                Text(state.units.rawValue)
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Basal")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let basal = state.basal
+                                Text(basal.formatted())
+                                Text(NSLocalizedString(" U/h", comment: " Units per hour"))
+                                    .foregroundColor(.secondary)
+                            }
+                            HStack {
+                                Text("Fraction")
+                                    .foregroundColor(.secondary)
+                                Spacer()
+                                let fraction = state.fraction
+                                Text(fraction.formatted())
+                            }
+                            if state.useFattyMealCorrectionFactor {
+                                HStack {
+                                    Text("Fatty Meal Factor")
+                                        .foregroundColor(.orange)
+                                    Spacer()
+                                    let fraction = state.fattyMealFactor
+                                    Text(fraction.formatted())
+                                        .foregroundColor(.orange)
+                                }
+                            }
+                        }.padding()
+                    }
+
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+
+                    VStack(spacing: Config.spacing) {
+                        HStack {
+                            Text("Glucose")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let glucose = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+                            Text(glucose.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                            Text(state.units.rawValue)
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let targetDifferenceInsulin = state.targetDifferenceInsulin
+                            // rounding
+                            let targetDifferenceInsulinAsDouble = NSDecimalNumber(decimal: targetDifferenceInsulin).doubleValue
+                            let roundedTargetDifferenceInsulin = Decimal(round(100 * targetDifferenceInsulinAsDouble) / 100)
+                            Text(roundedTargetDifferenceInsulin.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("IOB")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let iob = state.iob
+                            // rounding
+                            let iobAsDouble = NSDecimalNumber(decimal: iob).doubleValue
+                            let roundedIob = Decimal(round(100 * iobAsDouble) / 100)
+                            Text(roundedIob.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                            Spacer()
+
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let iobCalc = state.iobInsulinReduction
+                            // rounding
+                            let iobCalcAsDouble = NSDecimalNumber(decimal: iobCalc).doubleValue
+                            let roundedIobCalc = Decimal(round(100 * iobCalcAsDouble) / 100)
+                            Text(roundedIobCalc.formatted())
+                            Text(unit).foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("Trend")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let trend = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+                            Text(trend.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                            Text(state.units.rawValue).foregroundColor(.secondary)
+                            Spacer()
+
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let trendInsulin = state.fifteenMinInsulin
+                            // rounding
+                            let trendInsulinAsDouble = NSDecimalNumber(decimal: trendInsulin).doubleValue
+                            let roundedTrendInsulin = Decimal(round(100 * trendInsulinAsDouble) / 100)
+                            Text(roundedTrendInsulin.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                        HStack {
+                            Text("COB")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let cob = state.cob
+                            Text(cob.formatted())
+
+                            let unitGrams = NSLocalizedString(" g", comment: "grams")
+                            Text(unitGrams).foregroundColor(.secondary)
+
+                            Spacer()
+
+                            Image(systemName: "arrow.right")
+                            Spacer()
+
+                            let insulinCob = state.wholeCobInsulin
+                            // rounding
+                            let insulinCobAsDouble = NSDecimalNumber(decimal: insulinCob).doubleValue
+                            let roundedInsulinCob = Decimal(round(100 * insulinCobAsDouble) / 100)
+                            Text(roundedInsulinCob.formatted())
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding()
+
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+
+                    VStack {
+                        HStack {
+                            Text("Full Bolus")
+                                .foregroundColor(.secondary)
+                            Spacer()
+                            let insulin = state.roundedWholeCalc
+                            Text(insulin.formatted()).foregroundStyle(state.roundedWholeCalc < 0 ? Color.loopRed : Color.primary)
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding(.horizontal)
+
+                    Divider().frame(height: Config.dividerHeight)
+
+                    VStack {
+                        HStack {
+                            Text("Result")
+                                .fontWeight(.bold)
+                            Spacer()
+                            let fraction = state.fraction
+                            Text(fraction.formatted())
+                            Text(" x ")
+                                .foregroundColor(.secondary)
+
+                            // if fatty meal is chosen
+                            if state.useFattyMealCorrectionFactor {
+                                let fattyMealFactor = state.fattyMealFactor
+                                Text(fattyMealFactor.formatted())
+                                    .foregroundColor(.orange)
+                                Text(" x ")
+                                    .foregroundColor(.secondary)
+                            }
+
+                            let insulin = state.roundedWholeCalc
+                            Text(insulin.formatted()).foregroundStyle(state.roundedWholeCalc < 0 ? Color.loopRed : Color.primary)
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                            Text(" = ")
+                                .foregroundColor(.secondary)
+
+                            let result = state.insulinCalculated
+                            // rounding
+                            let resultAsDouble = NSDecimalNumber(decimal: result).doubleValue
+                            let roundedResult = Decimal(round(100 * resultAsDouble) / 100)
+                            Text(roundedResult.formatted())
+                                .fontWeight(.bold)
+                                .font(.system(size: 16))
+                                .foregroundColor(.blue)
+                            Text(unit)
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding()
+
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+
+                    if exceededMaxBolus {
+                        HStack {
+                            let maxBolus = state.maxBolus
+                            let maxBolusFormatted = maxBolus.formatted()
+                            Text("Your entered amount was limited by your max Bolus setting of \(maxBolusFormatted)\(unit)!")
+                        }
+                        .padding()
+                        .fontWeight(.semibold)
+                        .foregroundStyle(Color.loopRed)
+                    }
+                }
+                .padding(.top, 10)
+                .padding(.bottom, 15)
+
+                // Hide pop-up
+                VStack {
+                    Button {
+                        showInfo = false
+                    }
+                    label: {
+                        Text("OK")
+                    }
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    .font(.system(size: 16))
+                    .fontWeight(.semibold)
+                    .foregroundColor(.blue)
+                }
+                .padding(.bottom, 20)
+            }
+            .font(.footnote)
+            .background(
+                RoundedRectangle(cornerRadius: 10, style: .continuous)
+                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4).opacity(0.9))
+            )
+        }
+    }
+}

+ 8 - 261
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -5,275 +5,22 @@ extension Bolus {
     struct RootView: BaseView {
     struct RootView: BaseView {
         let resolver: Resolver
         let resolver: Resolver
         let waitForSuggestion: Bool
         let waitForSuggestion: Bool
+        let fetch: Bool
         @StateObject var state = StateModel()
         @StateObject var state = StateModel()
 
 
-        @State private var isAddInsulinAlertPresented = false
-        @State private var presentInfo = false
-        @State private var displayError = false
-
-        @Environment(\.colorScheme) var colorScheme
-
-        private var formatter: NumberFormatter {
-            let formatter = NumberFormatter()
-            formatter.numberStyle = .decimal
-            formatter.maximumFractionDigits = 2
-            return formatter
-        }
-
-        private var fractionDigits: Int {
-            if state.units == .mmolL {
-                return 1
-            } else { return 0 }
-        }
-
         var body: some View {
         var body: some View {
-            Form {
-                Section {
-                    if state.waitForSuggestion {
-                        HStack {
-                            Text("Wait please").foregroundColor(.secondary)
-                            Spacer()
-                            ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
-                        }
-                    } else {
-                        HStack {
-                            Text("Insulin recommended")
-                            Image(systemName: "info.bubble")
-                                .symbolRenderingMode(.palette)
-                                .foregroundStyle(.primary, .blue)
-                                .onTapGesture {
-                                    presentInfo.toggle()
-                                }
-
-                            Spacer()
-
-                            Text(
-                                formatter
-                                    .string(from: state.insulinRecommended as NSNumber)! +
-                                    NSLocalizedString(" U", comment: "Insulin unit")
-                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
-                                .onTapGesture {
-                                    if state.error, state.insulinRecommended > 0 { displayError = true }
-                                    else { state.amount = state.insulinRecommended }
-                                }
-                        }.contentShape(Rectangle())
-                    }
-                }
-                header: { Text("Recommendation") }
-                if !state.waitForSuggestion {
-                    Section {
-                        HStack {
-                            Text("Amount")
-                            Spacer()
-                            DecimalTextField(
-                                "0",
-                                value: $state.amount,
-                                formatter: formatter,
-                                autofocus: true,
-                                cleanInput: true
-                            )
-                            Text(!(state.amount > state.maxBolus) ? "U" : "😵").foregroundColor(.secondary)
-                        }
-                    }
-                    header: { Text("Bolus") }
-                    Section {
-                        Button { state.add() }
-                        label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .disabled(
-                                state.amount <= 0 || state.amount > state.maxBolus
-                            )
-                    }
-
-                    if waitForSuggestion {
-                        Section {
-                            Button { state.showModal(for: nil) }
-                            label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
-                        }
-                    }
-                }
-            }
-            .alert(isPresented: $displayError) {
-                Alert(
-                    title: Text("Warning!"),
-                    message: Text("\n" + alertString() + "\n"),
-                    primaryButton: .destructive(
-                        Text("Add"),
-                        action: {
-                            state.amount = state.insulinRecommended
-                            displayError = false
-                        }
-                    ),
-                    secondaryButton: .cancel()
-                )
-            }.onAppear {
-                configureView {
-                    state.waitForSuggestionInitial = waitForSuggestion
-                    state.waitForSuggestion = waitForSuggestion
-                }
-            }
-            .navigationTitle("Enact Bolus")
-            .navigationBarTitleDisplayMode(.inline)
-            .navigationBarItems(leading: Button("Close", action: state.hideModal))
-            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
-                bolusInfo
-            }
-        }
-
-        var bolusInfo: some View {
-            VStack {
-                // Variables
-                VStack(spacing: 3) {
-                    HStack {
-                        Text("Eventual Glucose").foregroundColor(.secondary)
-                        let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
-                        Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("Target Glucose").foregroundColor(.secondary)
-                        let target = state.units == .mmolL ? state.target.asMmolL : state.target
-                        Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
-                        Text(state.units.rawValue).foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("ISF").foregroundColor(.secondary)
-                        let isf = state.isf
-                        Text(isf.formatted())
-                        Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
-                            .foregroundColor(.secondary)
-                    }
-                    HStack {
-                        Text("ISF:")
-                        Text("Insulin Sensitivity")
-                    }.foregroundColor(.secondary).italic()
-                    if state.percentage != 100 {
-                        HStack {
-                            Text("Percentage setting").foregroundColor(.secondary)
-                            let percentage = state.percentage
-                            Text(percentage.formatted())
-                            Text("%").foregroundColor(.secondary)
-                        }
-                    }
-                    HStack {
-                        Text("Formula:")
-                        Text("(Eventual Glucose - Target) / ISF")
-                    }.foregroundColor(.secondary).italic().padding(.top, 5)
-                }
-                .font(.footnote)
-                .padding(.top, 10)
-                Divider()
-                // Formula
-                VStack(spacing: 5) {
-                    let unit = NSLocalizedString(
-                        " U",
-                        comment: "Unit in number of units delivered (keep the space character!)"
-                    )
-                    let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
-                    let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
-                    HStack {
-                        Text(NSLocalizedString("Insulin recommended", comment: "") + ":").font(.callout)
-                        Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
-                    }
-                    if state.percentage != 100, state.insulin > 0 {
-                        Divider()
-                        HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
-                            Text(
-                                state.insulinRecommended.formatted() + unit
-                            ).font(.callout).foregroundColor(.blue).bold()
-                        }
-                    }
-                }
-                // Warning
-                if state.error, state.insulinRecommended > 0 {
-                    VStack(spacing: 5) {
-                        Divider()
-                        Text("Warning!").font(.callout).bold().foregroundColor(.orange)
-                        Text(alertString()).font(.footnote)
-                        Divider()
-                    }.padding(.horizontal, 10)
-                }
-                // Footer
-                if !(state.error && state.insulinRecommended > 0) {
-                    VStack {
-                        Text(
-                            "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
-                        ).font(.caption2).foregroundColor(.secondary)
-                    }.padding(20)
-                }
-                // Hide button
-                VStack {
-                    Button { presentInfo = false }
-                    label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
-                        .foregroundColor(.blue)
-                }.padding(.bottom, 10)
-            }
-            .background(
-                RoundedRectangle(cornerRadius: 8, style: .continuous)
-                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
-                // .fill(Color(.systemGray).gradient)  // A more prominent pop-up, but harder to read
-            )
-        }
-
-        // Localize the Oref0 error/warning strings. The default should never be returned
-        private func alertString() -> String {
-            switch state.errorString {
-            case 1,
-                 2:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) + state.minGuardBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state.units
-                    .rawValue + ", " +
-                    NSLocalizedString(
-                        "which is below your Threshold (",
-                        comment: "Bolus pop-up / Alert string. Make translations concise!"
-                    ) + state
-                    .threshold.formatted() + " " + state.units.rawValue + ")"
-            case 3:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) +
-                    state.expectedDelta
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    NSLocalizedString(". Climbing: ", comment: "Bolus pop-up / Alert string. Make translatons concise!") + state
-                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-            case 4:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) +
-                    state.expectedDelta
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    NSLocalizedString(". Falling: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
-                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-            case 5:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is changing faster than expected. Expected: ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) +
-                    state.expectedDelta
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
-                    NSLocalizedString(". Changing: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
-                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
-            case 6:
-                return NSLocalizedString(
-                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
-                    comment: "Bolus pop-up / Alert string. Make translations concise!"
-                ) + state
-                    .minPredBG
-                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state
-                    .units
-                    .rawValue
-            default:
-                return "Ignore Warning..."
+            if state.useCalc {
+                // show alternative bolus calc based on toggle in bolus calc settings
+                AlternativeBolusCalcRootView(resolver: resolver, waitForSuggestion: waitForSuggestion, fetch: fetch, state: state)
+            } else {
+                // show iAPS standard bolus calc
+                DefaultBolusCalcRootView(resolver: resolver, waitForSuggestion: waitForSuggestion, fetch: fetch, state: state)
             }
             }
         }
         }
     }
     }
 }
 }
 
 
+// fix iOS 15 bug
 struct ActivityIndicator: UIViewRepresentable {
 struct ActivityIndicator: UIViewRepresentable {
     @Binding var isAnimating: Bool
     @Binding var isAnimating: Bool
     let style: UIActivityIndicatorView.Style
     let style: UIActivityIndicatorView.Style

+ 275 - 0
FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift

@@ -0,0 +1,275 @@
+import SwiftUI
+import Swinject
+
+extension Bolus {
+    struct DefaultBolusCalcRootView: BaseView {
+        let resolver: Resolver
+        let waitForSuggestion: Bool
+        let fetch: Bool
+        @StateObject var state = StateModel()
+
+        @State private var isAddInsulinAlertPresented = false
+        @State private var presentInfo = false
+        @State private var displayError = false
+
+        @Environment(\.colorScheme) var colorScheme
+
+        private var formatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
+        private var fractionDigits: Int {
+            if state.units == .mmolL {
+                return 1
+            } else { return 0 }
+        }
+
+        var body: some View {
+            Form {
+                Section {
+                    if state.waitForSuggestion {
+                        HStack {
+                            Text("Wait please").foregroundColor(.secondary)
+                            Spacer()
+                            ActivityIndicator(isAnimating: .constant(true), style: .medium) // fix iOS 15 bug
+                        }
+                    } else {
+                        HStack {
+                            Text("Insulin recommended")
+                            Image(systemName: "info.bubble")
+                                .symbolRenderingMode(.palette)
+                                .foregroundStyle(.primary, .blue)
+                                .onTapGesture {
+                                    presentInfo.toggle()
+                                }
+
+                            Spacer()
+
+                            Text(
+                                formatter
+                                    .string(from: state.insulinRecommended as NSNumber)! +
+                                    NSLocalizedString(" U", comment: "Insulin unit")
+                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
+                                .onTapGesture {
+                                    if state.error, state.insulinRecommended > 0 { displayError = true }
+                                    else { state.amount = state.insulinRecommended }
+                                }
+                        }.contentShape(Rectangle())
+                    }
+                }
+                header: { Text("Recommendation") }
+                if !state.waitForSuggestion {
+                    Section {
+                        HStack {
+                            Text("Amount")
+                            Spacer()
+                            DecimalTextField(
+                                "0",
+                                value: $state.amount,
+                                formatter: formatter,
+                                autofocus: true,
+                                cleanInput: true
+                            )
+                            Text(!(state.amount > state.maxBolus) ? "U" : "😵").foregroundColor(.secondary)
+                        }
+                    }
+                    header: { Text("Bolus") }
+                    Section {
+                        Button { state.add() }
+                        label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
+                            .frame(maxWidth: .infinity, alignment: .center)
+                            .disabled(
+                                state.amount <= 0 || state.amount > state.maxBolus
+                            )
+                    }
+
+                    if waitForSuggestion {
+                        Section {
+                            Button { state.showModal(for: nil) }
+                            label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
+                        }
+                    }
+                }
+            }
+            .alert(isPresented: $displayError) {
+                Alert(
+                    title: Text("Warning!"),
+                    message: Text("\n" + alertString() + "\n"),
+                    primaryButton: .destructive(
+                        Text("Add"),
+                        action: {
+                            state.amount = state.insulinRecommended
+                            displayError = false
+                        }
+                    ),
+                    secondaryButton: .cancel()
+                )
+            }.onAppear {
+                configureView {
+                    state.waitForSuggestionInitial = waitForSuggestion
+                    state.waitForSuggestion = waitForSuggestion
+                }
+            }
+            .navigationTitle("Enact Bolus")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
+                bolusInfo
+            }
+        }
+
+        var bolusInfo: some View {
+            VStack {
+                // Variables
+                VStack(spacing: 3) {
+                    HStack {
+                        Text("Eventual Glucose").foregroundColor(.secondary)
+                        let evg = state.units == .mmolL ? Decimal(state.evBG).asMmolL : Decimal(state.evBG)
+                        Text(evg.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("Target Glucose").foregroundColor(.secondary)
+                        let target = state.units == .mmolL ? state.target.asMmolL : state.target
+                        Text(target.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))))
+                        Text(state.units.rawValue).foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF").foregroundColor(.secondary)
+                        let isf = state.isf
+                        Text(isf.formatted())
+                        Text(state.units.rawValue + NSLocalizedString("/U", comment: "/Insulin unit"))
+                            .foregroundColor(.secondary)
+                    }
+                    HStack {
+                        Text("ISF:")
+                        Text("Insulin Sensitivity")
+                    }.foregroundColor(.secondary).italic()
+                    if state.percentage != 100 {
+                        HStack {
+                            Text("Percentage setting").foregroundColor(.secondary)
+                            let percentage = state.percentage
+                            Text(percentage.formatted())
+                            Text("%").foregroundColor(.secondary)
+                        }
+                    }
+                    HStack {
+                        Text("Formula:")
+                        Text("(Eventual Glucose - Target) / ISF")
+                    }.foregroundColor(.secondary).italic().padding(.top, 5)
+                }
+                .font(.footnote)
+                .padding(.top, 10)
+                Divider()
+                // Formula
+                VStack(spacing: 5) {
+                    let unit = NSLocalizedString(
+                        " U",
+                        comment: "Unit in number of units delivered (keep the space character!)"
+                    )
+                    let color: Color = (state.percentage != 100 && state.insulin > 0) ? .secondary : .blue
+                    let fontWeight: Font.Weight = (state.percentage != 100 && state.insulin > 0) ? .regular : .bold
+                    HStack {
+                        Text(NSLocalizedString("Insulin recommended", comment: "") + ":").font(.callout)
+                        Text(state.insulin.formatted() + unit).font(.callout).foregroundColor(color).fontWeight(fontWeight)
+                    }
+                    if state.percentage != 100, state.insulin > 0 {
+                        Divider()
+                        HStack { Text(state.percentage.formatted() + " % ->").font(.callout).foregroundColor(.secondary)
+                            Text(
+                                state.insulinRecommended.formatted() + unit
+                            ).font(.callout).foregroundColor(.blue).bold()
+                        }
+                    }
+                }
+                // Warning
+                if state.error, state.insulinRecommended > 0 {
+                    VStack(spacing: 5) {
+                        Divider()
+                        Text("Warning!").font(.callout).bold().foregroundColor(.orange)
+                        Text(alertString()).font(.footnote)
+                        Divider()
+                    }.padding(.horizontal, 10)
+                }
+                // Footer
+                if !(state.error && state.insulinRecommended > 0) {
+                    VStack {
+                        Text(
+                            "Carbs and previous insulin are included in the glucose prediction, but if the Eventual Glucose is lower than the Target Glucose, a bolus will not be recommended."
+                        ).font(.caption2).foregroundColor(.secondary)
+                    }.padding(20)
+                }
+                // Hide button
+                VStack {
+                    Button { presentInfo = false }
+                    label: { Text("Hide") }.frame(maxWidth: .infinity, alignment: .center).font(.callout)
+                        .foregroundColor(.blue)
+                }.padding(.bottom, 10)
+            }
+            .background(
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(Color(colorScheme == .dark ? UIColor.systemGray4 : UIColor.systemGray4))
+            )
+        }
+
+        // Localize the Oref0 error/warning strings. The default should never be returned
+        private func alertString() -> String {
+            switch state.errorString {
+            case 1,
+                 2:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) + state.minGuardBG
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state.units
+                    .rawValue + ", " +
+                    NSLocalizedString(
+                        "which is below your Threshold (",
+                        comment: "Bolus pop-up / Alert string. Make translations concise!"
+                    ) + state
+                    .threshold.formatted() + " " + state.units.rawValue + ")"
+            case 3:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is climbing slower than expected. Expected: ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) +
+                    state.expectedDelta
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    NSLocalizedString(". Climbing: ", comment: "Bolus pop-up / Alert string. Make translatons concise!") + state
+                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+            case 4:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is falling faster than expected. Expected: ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) +
+                    state.expectedDelta
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    NSLocalizedString(". Falling: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
+                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+            case 5:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is changing faster than expected. Expected: ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) +
+                    state.expectedDelta
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    NSLocalizedString(". Changing: ", comment: "Bolus pop-up / Alert string. Make translations concise!") + state
+                    .minDelta.formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+            case 6:
+                return NSLocalizedString(
+                    "Eventual Glucose > Target Glucose, but glucose is predicted to first drop down to ",
+                    comment: "Bolus pop-up / Alert string. Make translations concise!"
+                ) + state
+                    .minPredBG
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) + " " + state
+                    .units
+                    .rawValue
+            default:
+                return "Ignore Warning..."
+            }
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum BolusCalculatorConfig {
+    enum Config {}
+}
+
+protocol BolusCalculatorConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigProvider.swift

@@ -0,0 +1,3 @@
+extension BolusCalculatorConfig {
+    final class Provider: BaseProvider, BolusCalculatorConfigProvider {}
+}

+ 27 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift

@@ -0,0 +1,27 @@
+import SwiftUI
+
+extension BolusCalculatorConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Published var overrideFactor: Decimal = 0
+        @Published var useCalc: Bool = false
+        @Published var fattyMeals: Bool = false
+        @Published var fattyMealFactor: Decimal = 0
+
+        override func subscribe() {
+            subscribeSetting(\.overrideFactor, on: $overrideFactor, initial: {
+                let value = max(min($0, 1.2), 0.1)
+                overrideFactor = value
+            }, map: {
+                $0
+            })
+            subscribeSetting(\.useCalc, on: $useCalc) { useCalc = $0 }
+            subscribeSetting(\.fattyMeals, on: $fattyMeals) { fattyMeals = $0 }
+            subscribeSetting(\.fattyMealFactor, on: $fattyMealFactor, initial: {
+                let value = max(min($0, 1.2), 0.1)
+                fattyMealFactor = value
+            }, map: {
+                $0
+            })
+        }
+    }
+}

+ 52 - 0
FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift

@@ -0,0 +1,52 @@
+import SwiftUI
+import Swinject
+
+extension BolusCalculatorConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        private var conversionFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 1
+
+            return formatter
+        }
+
+        var body: some View {
+            Form {
+                Section(header: Text("Calculator settings")) {
+                    HStack {
+                        Toggle("Use alternative Bolus Calculator", isOn: $state.useCalc)
+                    }
+                    HStack {
+                        Text("Override With A Factor Of ")
+                        Spacer()
+                        DecimalTextField("0.8", value: $state.overrideFactor, formatter: conversionFormatter)
+                    }
+                }
+                Section(header: Text("Fatty Meals")) {
+                    HStack {
+                        Toggle("Apply factor for fatty meals", isOn: $state.fattyMeals)
+                    }
+                    HStack {
+                        Text("Override With A Factor Of ")
+                        Spacer()
+                        DecimalTextField("0.7", value: $state.fattyMealFactor, formatter: conversionFormatter)
+                    }
+                }
+
+                Section(
+                    footer: Text(
+                        "This is another approach to the bolus calculator integrated in iAPS. If the toggle is on you use this bolus calculator and not the original iAPS calculator. At the end of the calculation a custom factor is applied as it is supposed to be when using smbs (default 0.8).\n\nYou can also add the option in your bolus calculator to apply another (!) customizable factor at the end of the calculation which could be useful for fatty meals, e.g Pizza (default 0.7)."
+                    )
+                )
+                    {}
+            }
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Bolus Calculator")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 1 - 1
FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift

@@ -33,7 +33,7 @@ extension DataTable {
 
 
         func deleteCarbs(_ treatement: Treatment) {
         func deleteCarbs(_ treatement: Treatment) {
             nightscoutManager.deleteCarbs(
             nightscoutManager.deleteCarbs(
-                at: treatement.date,
+                at: treatement.id,
                 isFPU: treatement.isFPU,
                 isFPU: treatement.isFPU,
                 fpuID: treatement.fpuID,
                 fpuID: treatement.fpuID,
                 syncID: treatement.id
                 syncID: treatement.id

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

@@ -40,7 +40,7 @@ extension DataTable {
                 let carbs = self.provider.carbs()
                 let carbs = self.provider.carbs()
                     .filter { !($0.isFPU ?? false) }
                     .filter { !($0.isFPU ?? false) }
                     .map {
                     .map {
-                        if let id = $0.id {
+                        if let id = $0.collectionID {
                             return Treatment(
                             return Treatment(
                                 units: units,
                                 units: units,
                                 type: .carbs,
                                 type: .carbs,
@@ -62,7 +62,7 @@ extension DataTable {
                             type: .fpus,
                             type: .fpus,
                             date: $0.createdAt,
                             date: $0.createdAt,
                             amount: $0.carbs,
                             amount: $0.carbs,
-                            id: $0.id,
+                            id: $0.collectionID,
                             isFPU: $0.isFPU,
                             isFPU: $0.isFPU,
                             fpuID: $0.fpuID,
                             fpuID: $0.fpuID,
                             note: $0.note
                             note: $0.note

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

@@ -196,7 +196,7 @@ extension Home {
         }
         }
 
 
         func addCarbs() {
         func addCarbs() {
-            showModal(for: .addCarbs)
+            showModal(for: .addCarbs(editMode: false))
         }
         }
 
 
         func runLoop() {
         func runLoop() {

+ 7 - 2
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -481,7 +481,7 @@ extension Home {
                 Rectangle().fill(Color.gray.opacity(0.3)).frame(height: 50 + geo.safeAreaInsets.bottom)
                 Rectangle().fill(Color.gray.opacity(0.3)).frame(height: 50 + geo.safeAreaInsets.bottom)
 
 
                 HStack {
                 HStack {
-                    Button { state.showModal(for: .addCarbs) }
+                    Button { state.showModal(for: .addCarbs(editMode: false)) }
                     label: {
                     label: {
                         ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
                         ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
                             Image("carbs")
                             Image("carbs")
@@ -511,7 +511,12 @@ extension Home {
                     .foregroundColor(.loopGreen)
                     .foregroundColor(.loopGreen)
                     .buttonStyle(.borderless)
                     .buttonStyle(.borderless)
                     Spacer()
                     Spacer()
-                    Button { state.showModal(for: .bolus(waitForSuggestion: false)) }
+                    Button {
+                        state.showModal(for: .bolus(
+                            waitForSuggestion: true,
+                            fetch: false
+                        ))
+                    }
                     label: {
                     label: {
                         Image("bolus")
                         Image("bolus")
                             .renderingMode(.template)
                             .renderingMode(.template)

+ 2 - 0
FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift

@@ -8,9 +8,11 @@ extension PreferencesEditor {
         @Published var insulinReqPercentage: Decimal = 70
         @Published var insulinReqPercentage: Decimal = 70
         @Published var skipBolusScreenAfterCarbs = false
         @Published var skipBolusScreenAfterCarbs = false
         @Published var sections: [FieldSection] = []
         @Published var sections: [FieldSection] = []
+        @Published var useAlternativeBolusCalc: Bool = false
 
 
         override func subscribe() {
         override func subscribe() {
             preferences = provider.preferences
             preferences = provider.preferences
+            useAlternativeBolusCalc = settingsManager.settings.useCalc
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
             subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }

+ 5 - 3
FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift

@@ -30,9 +30,11 @@ extension PreferencesEditor {
 
 
                     Toggle("Remote control", isOn: $state.allowAnnouncements)
                     Toggle("Remote control", isOn: $state.allowAnnouncements)
 
 
-                    HStack {
-                        Text("Recommended Bolus Percentage")
-                        DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)
+                    if !state.useAlternativeBolusCalc {
+                        HStack {
+                            Text("Recommended Bolus Percentage")
+                            DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)
+                        }
                     }
                     }
 
 
                     Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)
                     Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -43,6 +43,7 @@ extension Settings {
                     Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
                     Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
                     Text("Target Glucose").navigationLink(to: .targetsEditor, from: self)
                     Text("Target Glucose").navigationLink(to: .targetsEditor, from: self)
                     Text("Autotune").navigationLink(to: .autotuneConfig, from: self)
                     Text("Autotune").navigationLink(to: .autotuneConfig, from: self)
+                    Text("Bolus Calculator").navigationLink(to: .bolusCalculatorConfig, from: self)
                 }
                 }
 
 
                 Section(header: Text("Developer")) {
                 Section(header: Text("Developer")) {

+ 9 - 6
FreeAPS/Sources/Router/Screen.swift

@@ -14,9 +14,9 @@ enum Screen: Identifiable, Hashable {
     case crEditor
     case crEditor
     case targetsEditor
     case targetsEditor
     case preferencesEditor
     case preferencesEditor
-    case addCarbs
+    case addCarbs(editMode: Bool)
     case addTempTarget
     case addTempTarget
-    case bolus(waitForSuggestion: Bool)
+    case bolus(waitForSuggestion: Bool, fetch: Bool)
     case manualTempBasal
     case manualTempBasal
     case autotuneConfig
     case autotuneConfig
     case dataTable
     case dataTable
@@ -32,6 +32,7 @@ enum Screen: Identifiable, Hashable {
     case statistics
     case statistics
     case watch
     case watch
     case statisticsConfig
     case statisticsConfig
+    case bolusCalculatorConfig
 
 
     var id: Int { String(reflecting: self).hashValue }
     var id: Int { String(reflecting: self).hashValue }
 }
 }
@@ -63,12 +64,12 @@ extension Screen {
             TargetsEditor.RootView(resolver: resolver)
             TargetsEditor.RootView(resolver: resolver)
         case .preferencesEditor:
         case .preferencesEditor:
             PreferencesEditor.RootView(resolver: resolver)
             PreferencesEditor.RootView(resolver: resolver)
-        case .addCarbs:
-            AddCarbs.RootView(resolver: resolver)
+        case let .addCarbs(editMode):
+            AddCarbs.RootView(resolver: resolver, editMode: editMode)
         case .addTempTarget:
         case .addTempTarget:
             AddTempTarget.RootView(resolver: resolver)
             AddTempTarget.RootView(resolver: resolver)
-        case let .bolus(waitForSuggestion):
-            Bolus.RootView(resolver: resolver, waitForSuggestion: waitForSuggestion)
+        case let .bolus(waitForSuggestion, fetch):
+            Bolus.RootView(resolver: resolver, waitForSuggestion: waitForSuggestion, fetch: fetch)
         case .manualTempBasal:
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
             ManualTempBasal.RootView(resolver: resolver)
         case .autotuneConfig:
         case .autotuneConfig:
@@ -99,6 +100,8 @@ extension Screen {
             Stat.RootView(resolver: resolver)
             Stat.RootView(resolver: resolver)
         case .statisticsConfig:
         case .statisticsConfig:
             StatConfig.RootView(resolver: resolver)
             StatConfig.RootView(resolver: resolver)
+        case .bolusCalculatorConfig:
+            BolusCalculatorConfig.RootView(resolver: resolver)
         }
         }
     }
     }
 
 

+ 1 - 1
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -194,7 +194,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             let sampleIDs = samples.compactMap(\.syncIdentifier)
             let sampleIDs = samples.compactMap(\.syncIdentifier)
             let sampleDates = samples.map(\.startDate)
             let sampleDates = samples.map(\.startDate)
             let samplesToSave = carbsWithId
             let samplesToSave = carbsWithId
-                .filter { !sampleIDs.contains($0.id!) } // id existing in AH
+                .filter { !sampleIDs.contains($0.collectionID!) } // id existing in AH
                 .filter { !sampleDates.contains($0.createdAt) } // not id but exaclty the same datetime
                 .filter { !sampleDates.contains($0.createdAt) } // not id but exaclty the same datetime
                 .map {
                 .map {
                     HKQuantitySample(
                     HKQuantitySample(

+ 6 - 5
FreeAPS/Sources/Services/Network/NightscoutAPI.swift

@@ -141,17 +141,18 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
             .eraseToAnyPublisher()
     }
     }
 
 
-    func deleteCarbs(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+    func deleteCarbs(at uniqueID: String) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         var components = URLComponents()
         components.scheme = url.scheme
         components.scheme = url.scheme
         components.host = url.host
         components.host = url.host
         components.port = url.port
         components.port = url.port
         components.path = Config.treatmentsPath
         components.path = Config.treatmentsPath
         components.queryItems = [
         components.queryItems = [
-            URLQueryItem(name: "find[carbs][$exists]", value: "true"),
+            // Removed below because it prevented all futire entries to be deleted. Don't know why?
+            /* URLQueryItem(name: "find[carbs][$exists]", value: "true"), */
             URLQueryItem(
             URLQueryItem(
-                name: "find[created_at][$eq]",
-                value: Formatter.iso8601withFractionalSeconds.string(from: date)
+                name: "find[collectionID][$eq]",
+                value: uniqueID
             )
             )
         ]
         ]
 
 
@@ -322,7 +323,7 @@ extension NightscoutAPI {
         if let secret = secret {
         if let secret = secret {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
         }
-        request.httpBody = try! JSONCoding.encoder.encode(treatments)
+        request.httpBody = try? JSONCoding.encoder.encode(treatments)
         request.httpMethod = "POST"
         request.httpMethod = "POST"
 
 
         return service.run(request)
         return service.run(request)

+ 18 - 48
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -9,7 +9,7 @@ protocol NightscoutManager: GlucoseSource {
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
     func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
     func fetchTempTargets() -> AnyPublisher<[TempTarget], Never>
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
     func fetchAnnouncements() -> AnyPublisher<[Announcement], Never>
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
+    func deleteCarbs(at uniqueID: String, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
     func deleteInsulin(at date: Date)
     func deleteManualGlucose(at: Date)
     func deleteManualGlucose(at: Date)
     func uploadStatus()
     func uploadStatus()
@@ -177,62 +177,32 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .eraseToAnyPublisher()
             .eraseToAnyPublisher()
     }
     }
 
 
-    func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String) {
+    func deleteCarbs(at uniqueID: String, isFPU: Bool?, fpuID: String?, syncID: String) {
         // remove in AH
         // remove in AH
         healthkitManager.deleteCarbs(syncID: syncID, isFPU: isFPU, fpuID: fpuID)
         healthkitManager.deleteCarbs(syncID: syncID, isFPU: isFPU, fpuID: fpuID)
 
 
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
-            carbsStorage.deleteCarbs(at: date)
+            carbsStorage.deleteCarbs(at: uniqueID)
             return
             return
         }
         }
 
 
-        if let isFPU = isFPU, isFPU {
-            guard let fpuID = fpuID else { return }
-            let allValues = storage.retrieve(OpenAPS.Monitor.carbHistory, as: [CarbsEntry].self) ?? []
-            let dates = allValues.filter { $0.fpuID == fpuID }.map(\.createdAt).removeDublicates()
-
-            let publishers = dates
-                .map { d -> AnyPublisher<Void, Swift.Error> in
-                    nightscout.deleteCarbs(
-                        at: d
+        nightscout.deleteCarbs(at: uniqueID)
+            .collect()
+            .sink { completion in
+                self.carbsStorage.deleteCarbs(at: uniqueID)
+                switch completion {
+                case .finished:
+                    debug(.nightscout, "Carbs deleted")
+                case let .failure(error):
+                    info(
+                        .nightscout,
+                        "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
+                        type: MessageType.warning
                     )
                     )
                 }
                 }
-
-            Publishers.MergeMany(publishers)
-                .collect()
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: date)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carbs deleted")
-
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: { _ in }
-                .store(in: &lifetime)
-
-        } else {
-            nightscout.deleteCarbs(at: date)
-                .sink { completion in
-                    self.carbsStorage.deleteCarbs(at: date)
-                    switch completion {
-                    case .finished:
-                        debug(.nightscout, "Carbs deleted")
-                    case let .failure(error):
-                        info(
-                            .nightscout,
-                            "Deletion of carbs in NightScout not done \n \(error.localizedDescription)",
-                            type: MessageType.warning
-                        )
-                    }
-                } receiveValue: {}
-                .store(in: &lifetime)
-        }
+            } receiveValue: { _ in }
+            .store(in: &lifetime)
+        // }
     }
     }
 
 
     func deleteInsulin(at date: Date) {
     func deleteInsulin(at date: Date) {

+ 1 - 1
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -272,7 +272,7 @@ extension BaseWatchManager: WCSessionDelegate {
         {
         {
             carbsStorage.storeCarbs(
             carbsStorage.storeCarbs(
                 [CarbsEntry(
                 [CarbsEntry(
-                    id: UUID().uuidString,
+                    collectionID: UUID().uuidString,
                     createdAt: Date(),
                     createdAt: Date(),
                     carbs: Decimal(carbs),
                     carbs: Decimal(carbs),
                     fat: Decimal(fat),
                     fat: Decimal(fat),

+ 1 - 1
FreeAPS/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift

@@ -11,7 +11,7 @@ import Foundation
 
 
         carbsStorage.storeCarbs(
         carbsStorage.storeCarbs(
             [CarbsEntry(
             [CarbsEntry(
-                id: UUID().uuidString,
+                collectionID: UUID().uuidString,
                 createdAt: dateAdded,
                 createdAt: dateAdded,
                 carbs: carbs,
                 carbs: carbs,
                 fat: Decimal(quantityFat),
                 fat: Decimal(quantityFat),

+ 0 - 25
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -30,31 +30,6 @@ struct DecimalTextField: UIViewRepresentable {
         textfield.text = cleanInput ? "" : formatter.string(for: value) ?? placeholder
         textfield.text = cleanInput ? "" : formatter.string(for: value) ?? placeholder
         textfield.textAlignment = .right
         textfield.textAlignment = .right
 
 
-        let toolBar = UIToolbar(frame: CGRect(
-            x: 0,
-            y: 0,
-            width: textfield.frame.size.width,
-            height: 44
-        ))
-        let clearButton = UIBarButtonItem(
-            title: NSLocalizedString("Clear", comment: "Clear button"),
-            style: .plain,
-            target: self,
-            action: #selector(textfield.clearButtonTapped(button:))
-        )
-        let doneButton = UIBarButtonItem(
-            title: NSLocalizedString("Done", comment: "Done button"),
-            style: .done,
-            target: self,
-            action: #selector(textfield.doneButtonTapped(button:))
-        )
-        let space = UIBarButtonItem(
-            barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
-            target: nil,
-            action: nil
-        )
-        toolBar.setItems([clearButton, space, doneButton], animated: true)
-        textfield.inputAccessoryView = toolBar
         if autofocus {
         if autofocus {
             DispatchQueue.main.async {
             DispatchQueue.main.async {
                 textfield.becomeFirstResponder()
                 textfield.becomeFirstResponder()