Browse Source

Merge branch 'dev' into Crowdin

Jon B.M 2 years ago
parent
commit
ce672f153f
63 changed files with 1951 additions and 668 deletions
  1. 2 0
      .gitignore
  2. 1 1
      Config.xcconfig
  3. 9 1
      Core_Data.xcdatamodeld/Core_Data.xcdatamodel/contents
  4. 61 5
      FreeAPS.xcodeproj/project.pbxproj
  5. 1 1
      FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved
  6. 4 4
      FreeAPS/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json
  7. 31 0
      FreeAPS/Resources/Assets.xcassets/owl.imageset/Contents.json
  8. BIN
      FreeAPS/Resources/Assets.xcassets/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3 (kopia).png
  9. BIN
      FreeAPS/Resources/Assets.xcassets/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3.png
  10. 0 2
      FreeAPS/Resources/Info.plist
  11. 5 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  12. 23 1
      FreeAPS/Sources/APS/APSManager.swift
  13. 1 1
      FreeAPS/Sources/APS/CGM/DexcomSourceG5.swift
  14. 1 1
      FreeAPS/Sources/APS/CGM/DexcomSourceG6.swift
  15. 13 2
      FreeAPS/Sources/APS/CGM/dexcomSourceG7.swift
  16. 0 1
      FreeAPS/Sources/APS/OpenAPS/OpenAPS.swift
  17. 11 0
      FreeAPS/Sources/APS/Storage/AnnouncementsStorage.swift
  18. 11 20
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  19. 14 2
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  20. 3 0
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  21. 39 0
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  22. 1 1
      FreeAPS/Sources/Models/Announcement.swift
  23. 2 2
      FreeAPS/Sources/Models/CarbsEntry.swift
  24. 20 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  25. 2 0
      FreeAPS/Sources/Models/NightscoutTreatment.swift
  26. 7 2
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  27. 2 0
      FreeAPS/Sources/Models/Suggestion.swift
  28. 51 14
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  29. 14 7
      FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift
  30. 7 7
      FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift
  31. 148 35
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  32. 23 0
      FreeAPS/Sources/Modules/Bolus/Components/CheckboxToggleStyle.swift
  33. 495 0
      FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift
  34. 8 301
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  35. 355 0
      FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift
  36. 5 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigDataFlow.swift
  37. 3 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorConfigProvider.swift
  38. 27 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/BolusCalculatorStateModel.swift
  39. 52 0
      FreeAPS/Sources/Modules/BolusCalculatorConfig/View/BolusCalculatorConfigRootView.swift
  40. 15 8
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  41. 7 1
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  42. 46 8
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  43. 182 63
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  44. 1 0
      FreeAPS/Sources/Modules/Home/HomeDataFlow.swift
  45. 7 0
      FreeAPS/Sources/Modules/Home/HomeProvider.swift
  46. 11 1
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  47. 77 0
      FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift
  48. 11 5
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  49. 3 1
      FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift
  50. 10 4
      FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift
  51. 15 13
      FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift
  52. 2 6
      FreeAPS/Sources/Modules/PreferencesEditor/PreferencesEditorStateModel.swift
  53. 5 3
      FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift
  54. 23 5
      FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift
  55. 5 3
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  56. 1 1
      FreeAPS/Sources/Modules/Stat/View/StatRootView.swift
  57. 9 6
      FreeAPS/Sources/Router/Screen.swift
  58. 18 26
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  59. 6 5
      FreeAPS/Sources/Services/Network/NightscoutAPI.swift
  60. 43 70
      FreeAPS/Sources/Services/Network/NightscoutManager.swift
  61. 1 1
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  62. 1 1
      FreeAPS/Sources/Shortcuts/Carbs/CarbPresetIntentRequest.swift
  63. 0 25
      FreeAPS/Sources/Views/DecimalTextField.swift

+ 2 - 0
.gitignore

@@ -80,3 +80,5 @@ fastlane/test_output
 fastlane/FastlaneRunner
 
 ConfigOverride.xcconfig
+
+branch.txt

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = iAPS
-APP_VERSION = 2.2.5
+APP_VERSION = 2.2.7
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 DEVELOPER_TEAM = ##TEAM_ID##

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

@@ -1,5 +1,5 @@
 <?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">
         <attribute name="average" 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="start" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
     </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">
         <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"/>

File diff suppressed because it is too large
+ 61 - 5
FreeAPS.xcodeproj/project.pbxproj


+ 1 - 1
FreeAPS.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -30,7 +30,7 @@
       },
       {
         "package": "SwiftCharts",
-        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts",
+        "repositoryURL": "https://github.com/ivanschuetz/SwiftCharts.git",
         "state": {
           "branch": "master",
           "revision": "c354c1945bb35a1f01b665b22474f6db28cba4a2",

+ 4 - 4
FreeAPS/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json

@@ -22,10 +22,10 @@
       "color" : {
         "color-space" : "srgb",
         "components" : {
-          "alpha" : "1.000",
-          "blue" : "1.000",
-          "green" : "1.000",
-          "red" : "1.000"
+          "alpha" : "0.500",
+          "blue" : "0.988",
+          "green" : "0.588",
+          "red" : "0.118"
         }
       },
       "idiom" : "universal"

+ 31 - 0
FreeAPS/Resources/Assets.xcassets/owl.imageset/Contents.json

@@ -0,0 +1,31 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "light"
+        }
+      ],
+      "filename" : "apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3.png",
+      "idiom" : "universal"
+    },
+    {
+      "appearances" : [
+        {
+          "appearance" : "luminosity",
+          "value" : "dark"
+        }
+      ],
+      "filename" : "apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3 (kopia).png",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
FreeAPS/Resources/Assets.xcassets/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3 (kopia).png


BIN
FreeAPS/Resources/Assets.xcassets/owl.imageset/apps.53525.13510798887505226.836b071a-2d62-4ce8-92bc-701910b6b96e.19f04184-2123-4d05-89b5-97f5c4f7a432-2 3.png


+ 0 - 2
FreeAPS/Resources/Info.plist

@@ -8,8 +8,6 @@
 	<array>
 		<string>com.freeapsx.background-task.critical-event-log</string>
 	</array>
-	<key>BuildBranch</key>
-	<string></string>
 	<key>CBBundleDisplayName</key>
 	<string>$(APP_DISPLAY_NAME)</string>
 	<key>CFBundleDevelopmentRegion</key>

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

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

+ 23 - 1
FreeAPS/Sources/APS/APSManager.swift

@@ -948,7 +948,29 @@ final class BaseAPSManager: APSManager, Injectable {
                 let buildDate = Bundle.main.buildDate
                 let version = Bundle.main.releaseVersionNumber
                 let build = Bundle.main.buildVersionNumber
-                let branch = Bundle.main.infoDictionary?["BuildBranch"] as? String ?? ""
+
+                // Read branch information from branch.txt instead of infoDictionary
+                var branch = "Unknown"
+                if let branchFileURL = Bundle.main.url(forResource: "branch", withExtension: "txt"),
+                   let branchFileContent = try? String(contentsOf: branchFileURL)
+                {
+                    let lines = branchFileContent.components(separatedBy: .newlines)
+                    for line in lines {
+                        let components = line.components(separatedBy: "=")
+                        if components.count == 2 {
+                            let key = components[0].trimmingCharacters(in: .whitespaces)
+                            let value = components[1].trimmingCharacters(in: .whitespaces)
+
+                            if key == "BRANCH" {
+                                branch = value
+                                break
+                            }
+                        }
+                    }
+                } else {
+                    branch = "Unknown"
+                }
+
                 let copyrightNotice_ = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
                 let pump_ = pumpManager?.localizedTitle ?? ""
                 let cgm = settingsManager.settings.cgm

+ 1 - 1
FreeAPS/Sources/APS/CGM/DexcomSourceG5.swift

@@ -148,7 +148,7 @@ extension DexcomSourceG5: CGMManagerDelegate {
                 let quantity = newGlucoseSample.quantity
                 let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
                 return BloodGlucose(
-                    _id: newGlucoseSample.syncIdentifier,
+                    _id: UUID().uuidString,
                     sgv: value,
                     direction: .init(trendType: newGlucoseSample.trend),
                     date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),

+ 1 - 1
FreeAPS/Sources/APS/CGM/DexcomSourceG6.swift

@@ -153,7 +153,7 @@ extension DexcomSourceG6: CGMManagerDelegate {
                     let quantity = newGlucoseSample.quantity
                     let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
                     return BloodGlucose(
-                        _id: newGlucoseSample.syncIdentifier,
+                        _id: UUID().uuidString,
                         sgv: value,
                         direction: .init(trendType: newGlucoseSample.trend),
                         date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),

+ 13 - 2
FreeAPS/Sources/APS/CGM/dexcomSourceG7.swift

@@ -135,11 +135,20 @@ extension DexcomSourceG7: CGMManagerDelegate {
         debug(.deviceManager, "DEXCOMG7 - Process CGM Reading Result launched")
         switch readingResult {
         case let .newData(values):
+
+            var activationDate: Date = .distantPast
+            var sessionStart: Date = .distantPast
+            if let cgmG7Manager = cgmManager as? G7CGMManager {
+                activationDate = cgmG7Manager.sensorActivatedAt ?? .distantPast
+                sessionStart = cgmG7Manager.sensorFinishesWarmupAt ?? .distantPast
+                print("Activastion date: " + activationDate.description)
+            }
+
             let bloodGlucose = values.compactMap { newGlucoseSample -> BloodGlucose? in
                 let quantity = newGlucoseSample.quantity
                 let value = Int(quantity.doubleValue(for: .milligramsPerDeciliter))
                 return BloodGlucose(
-                    _id: newGlucoseSample.syncIdentifier,
+                    _id: UUID().uuidString,
                     sgv: value,
                     direction: .init(trendType: newGlucoseSample.trend),
                     date: Decimal(Int(newGlucoseSample.date.timeIntervalSince1970 * 1000)),
@@ -148,7 +157,9 @@ extension DexcomSourceG7: CGMManagerDelegate {
                     filtered: nil,
                     noise: nil,
                     glucose: value,
-                    type: "sgv"
+                    type: "sgv",
+                    activationDate: activationDate,
+                    sessionStartDate: sessionStart
                 )
             }
 

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

@@ -120,7 +120,6 @@ final class OpenAPS {
 
     func oref2() -> RawJSON {
         coredataContext.performAndWait {
-            let now = Date()
             let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self)
             var hbt_ = preferences?.halfBasalExerciseTarget ?? 160
             let wp = preferences?.weightPercentage ?? 1

+ 11 - 0
FreeAPS/Sources/APS/Storage/AnnouncementsStorage.swift

@@ -6,6 +6,7 @@ protocol AnnouncementsStorage {
     func storeAnnouncements(_ announcements: [Announcement], enacted: Bool)
     func syncDate() -> Date
     func recent() -> Announcement?
+    func validate() -> [Announcement]
 }
 
 final class BaseAnnouncementsStorage: AnnouncementsStorage, Injectable {
@@ -66,4 +67,14 @@ final class BaseAnnouncementsStorage: AnnouncementsStorage, Injectable {
         }
         return recent
     }
+
+    func validate() -> [Announcement] {
+        guard let enactedEvents = storage.retrieve(OpenAPS.FreeAPS.announcementsEnacted, as: [Announcement].self)?.reversed()
+        else {
+            return []
+        }
+        let validate = enactedEvents
+            .filter({ $0.enteredBy == Announcement.remote })
+        return validate
+    }
 }

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

@@ -12,7 +12,7 @@ protocol CarbsStorage {
     func syncDate() -> Date
     func recent() -> [CarbsEntry]
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment]
-    func deleteCarbs(at date: Date)
+    func deleteCarbs(at uniqueID: String)
 }
 
 final class BaseCarbsStorage: CarbsStorage, Injectable {
@@ -71,7 +71,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 // New date for each carb equivalent
                 var useDate = entries.last?.createdAt ?? Date()
                 // Group and Identify all FPUs together
-                let fpuID = UUID().uuidString
+                let fpuID = (entries.last?.collectionID ?? "") + ".fpu"
                 // Create an array of all future carb equivalents.
                 var futureCarbArray = [CarbsEntry]()
                 while carbEquivalents > 0, numberOfEquivalents > 0 {
@@ -81,7 +81,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
 
                     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,
                         fpuID: fpuID
                     )
@@ -101,7 +101,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
             } // ------------------------- END OF TPU ----------------------------------------
             // Store the actual (normal) carbs
             if entries.last?.carbs ?? 0 > 0 {
-                uniqEvents = []
+                // uniqEvents = []
                 self.storage.transaction { storage in
                     storage.append(entries, to: file, uniqBy: \.createdAt)
                     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() ?? []
     }
 
-    func deleteCarbs(at date: Date) {
+    func deleteCarbs(at uniqueID: String) {
         processQueue.sync {
             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 {
-                allValues.remove(at: entryIndex)
+                allValues.removeAll(where: { $0.collectionID == uniqueID })
                 storage.save(allValues, as: OpenAPS.Monitor.carbHistory)
                 broadcaster.notify(CarbsObserver.self, on: processQueue) {
                     $0.carbsDidUpdate(allValues)
@@ -170,7 +160,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
     }
 
     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 treatments = eventsManual.map {
@@ -190,7 +180,8 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 protein: nil,
                 foodType: $0.note,
                 targetTop: nil,
-                targetBottom: nil
+                targetBottom: nil,
+                collectionID: $0.collectionID
             )
         }
         return Array(Set(treatments).subtracting(Set(uploaded)))

+ 14 - 2
FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -45,7 +45,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         rate: nil,
                         temp: nil,
                         carbInput: nil,
-                        isSMB: dose.automatic
+                        isSMB: dose.automatic,
+                        isExternal: dose.manuallyEntered
                     )]
                 case .tempBasal:
                     guard let dose = event.dose else { return [] }
@@ -210,6 +211,16 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
+    func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
+        if event.isSMB ?? false {
+            return .smb
+        }
+        if event.isExternal ?? false {
+            return .isExternal
+        }
+        return event.type
+    }
+
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
         let events = recent()
         guard !events.isEmpty else { return [] }
@@ -250,13 +261,14 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         let bolusesAndCarbs = events.compactMap { event -> NigtscoutTreatment? in
             switch event.type {
             case .bolus:
+                let eventType = determineBolusEventType(for: event)
                 return NigtscoutTreatment(
                     duration: event.duration,
                     rawDuration: nil,
                     rawRate: nil,
                     absolute: nil,
                     rate: nil,
-                    eventType: (event.isSMB ?? false) ? .smb : .bolus,
+                    eventType: eventType,
                     createdAt: event.timestamp,
                     enteredBy: NigtscoutTreatment.local,
                     bolus: event,

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

@@ -1171,6 +1171,9 @@ Enact a temp Basal or a temp target */
 /* An Automatic delivered bolus (SMB) */
 "SMB" = "SMB";
 
+/* A manually entered dose of external insulin */
+"External Insulin" = "Externes Insulin";
+
 /* Status highlight when manual temp basal is running. */
 "Manual Basal" = "Manuelle Temporäre Basalrate";
 

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

@@ -55,6 +55,39 @@
 /* Headline in enacted pop up (at: at what time) */
  "Error at" = "Error at";
 
+/* Bolus View Meal Summary Header */
+"Meal Summary" = "Meal Summary";
+
+/* Bolus View Meal Edit Meal Button */
+"Edit Meal" = "Edit Meal";
+
+/* Bolus View Meal Add Meal Button */
+"Add Meal" = "Add Meal";
+
+/* Bolus View Bolus Summary Header */
+"Bolus Summary" = "Bolus Summary";
+
+/* For the  Bolus View pop-up */
+"Calculations" = "Calculations";
+
+/* For the  Bolus View pop-up */
+"Fatty Meal" = "Fatty Meal";
+
+/* For the  Bolus View pop-up */
+"Full Bolus" = "Full Bolus";
+
+/* For the  Bolus View pop-up */
+"Fraction" = "Fraction";
+
+/* For the  Bolus View pop-up */
+"Fatty Meal Factor" = "Fatty Meal Factor";
+
+/* For the  Bolus View pop-up */
+"Result" = "Result";
+
+/* For the  Bolus View pop-up */
+"Your entered amount was limited by your max Bolus setting of %d%@" = "Your entered amount was limited by your max Bolus setting of %d%@";
+
 /* Home title */
 "Home" = "Home";
 
@@ -569,6 +602,9 @@ Enact a temp Basal or a temp target */
 /* Automatic delivered treatments */
 "Automatic" = "Automatic";
 
+/* External insulin treatments */
+"External" = "External";
+
 /* */
 "Other" = "Other";
 
@@ -1172,6 +1208,9 @@ Enact a temp Basal or a temp target */
 /* An Automatic delivered bolus (SMB) */
 "SMB" = "SMB";
 
+/* A manually entered dose of external insulin */
+"External Insulin" = "External Insulin";
+
 /* Status highlight when manual temp basal is running. */
 "Manual Basal" = "Manual Basal";
 

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

@@ -1,6 +1,6 @@
 import Foundation
 
-struct Announcement: JSON {
+struct Announcement: JSON, Equatable, Hashable {
     let createdAt: Date
     let enteredBy: String
     let notes: String

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

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

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

@@ -45,6 +45,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var maxCarbs: Decimal = 1000
     var displayFatAndProteinOnWatch: 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 {
@@ -139,6 +143,22 @@ extension FreeAPSSettings: Decodable {
             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) {
             settings.timeCap = timeCap
         }

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

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

+ 7 - 2
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -12,6 +12,7 @@ struct PumpHistoryEvent: JSON, Equatable {
     let carbInput: Int?
     let note: String?
     let isSMB: Bool?
+    let isExternal: Bool?
 
     init(
         id: String,
@@ -24,7 +25,8 @@ struct PumpHistoryEvent: JSON, Equatable {
         temp: TempType? = nil,
         carbInput: Int? = nil,
         note: String? = nil,
-        isSMB: Bool? = nil
+        isSMB: Bool? = nil,
+        isExternal: Bool? = nil
     ) {
         self.id = id
         self.type = type
@@ -37,13 +39,15 @@ struct PumpHistoryEvent: JSON, Equatable {
         self.carbInput = carbInput
         self.note = note
         self.isSMB = isSMB
+        self.isExternal = isExternal
     }
 }
 
 enum EventType: String, JSON {
     case bolus = "Bolus"
     case smb = "SMB"
-    case mealBulus = "Meal Bolus"
+    case isExternal = "External Insulin"
+    case mealBolus = "Meal Bolus"
     case correctionBolus = "Correction Bolus"
     case snackBolus = "Snack Bolus"
     case bolusWizard = "BolusWizard"
@@ -86,5 +90,6 @@ extension PumpHistoryEvent {
         case carbInput = "carb_input"
         case note
         case isSMB
+        case isExternal
     }
 }

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

@@ -29,6 +29,7 @@ struct Suggestion: JSON, Equatable {
     let minGuardBG: Decimal?
     let minPredBG: Decimal?
     let threshold: Decimal?
+    let carbRatio: Decimal?
 }
 
 struct Predictions: JSON, Equatable {
@@ -75,6 +76,7 @@ extension Suggestion {
         case minGuardBG
         case minPredBG
         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 maxCarbs: Decimal = 0
         @Published var note: String = ""
+        @Published var id_: String = ""
+        @Published var summary: String = ""
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -33,25 +35,28 @@ extension AddCarbs {
                 return
             }
             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 {
                 apsManager.determineBasalSync()
                 showModal(for: nil)
+            } else if carbs > 0 {
+                saveToCoreData(carbsToStore)
+                showModal(for: .bolus(waitForSuggestion: true, fetch: true))
             } else {
-                showModal(for: .bolus(waitForSuggestion: true))
+                hideModal()
             }
         }
 
@@ -160,5 +165,37 @@ extension AddCarbs {
             }
             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()
+            }
+        }
     }
 }

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

@@ -5,9 +5,10 @@ import Swinject
 extension AddCarbs {
     struct RootView: BaseView {
         let resolver: Resolver
+        let editMode: Bool
         @StateObject var state = StateModel()
         @State var dish: String = ""
-        @State var isPromtPresented = false
+        @State var isPromptPresented = false
         @State var saved = false
         @State private var showAlert = false
         @FocusState private var isFocused: Bool
@@ -75,7 +76,7 @@ extension AddCarbs {
                             .controlSize(.mini)
                             .buttonStyle(BorderlessButtonStyle())
                         Button {
-                            isPromtPresented = true
+                            isPromptPresented = true
                         }
                         label: { Text("Save as Preset") }
                             .frame(maxWidth: .infinity, alignment: .trailing)
@@ -101,7 +102,7 @@ extension AddCarbs {
                                     )
                             )
                     }
-                    .popover(isPresented: $isPromtPresented) {
+                    .popover(isPresented: $isPromptPresented) {
                         presetPopover
                     }
                 }
@@ -118,7 +119,7 @@ extension AddCarbs {
 
                 Section {
                     Button { state.add() }
-                    label: { Text("Save and continue").font(.title3) }
+                    label: { Text(state.carbs > 0 ? "Continue" : "Save") }
                         .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                         .frame(maxWidth: .infinity, alignment: .center)
                 } footer: { Text(state.waitersNotepad().description) }
@@ -129,7 +130,13 @@ extension AddCarbs {
                     }
                 }
             }
-            .onAppear(perform: configureView)
+            .onAppear {
+                configureView {
+                    state.loadEntries(editMode)
+                }
+            }
+            .navigationTitle("Add Meals")
+            .navigationBarTitleDisplayMode(.inline)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
         }
 
@@ -148,14 +155,14 @@ extension AddCarbs {
                             try? moc.save()
                             state.addNewPresetToWaitersNotepad(dish)
                             saved = false
-                            isPromtPresented = false
+                            isPromptPresented = false
                         }
                     }
                     label: { Text("Save") }
                     Button {
                         dish = ""
                         saved = false
-                        isPromtPresented = false }
+                        isPromptPresented = false }
                     label: { Text("Cancel") }
                 } header: { Text("Enter Meal Preset Name") }
             }

+ 7 - 7
FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift

@@ -6,7 +6,7 @@ extension AddTempTarget {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
-        @State private var isPromtPresented = false
+        @State private var isPromptPresented = false
         @State private var isRemoveAlertPresented = false
         @State private var removeAlert: Alert?
         @State private var isEditing = false
@@ -99,7 +99,7 @@ extension AddTempTarget {
                             Text("minutes").foregroundColor(.secondary)
                         }
                         DatePicker("Date", selection: $state.date)
-                        Button { isPromtPresented = true }
+                        Button { isPromptPresented = true }
                         label: { Text("Save as preset") }
                     }
                 }
@@ -112,7 +112,7 @@ extension AddTempTarget {
                             Text("minutes").foregroundColor(.secondary)
                         }
                         DatePicker("Date", selection: $state.date)
-                        Button { isPromtPresented = true }
+                        Button { isPromptPresented = true }
                         label: { Text("Save as preset") }
                             .disabled(state.duration == 0)
                     }
@@ -125,16 +125,16 @@ extension AddTempTarget {
                     label: { Text("Cancel Temp Target") }
                 }
             }
-            .popover(isPresented: $isPromtPresented) {
+            .popover(isPresented: $isPromptPresented) {
                 Form {
                     Section(header: Text("Enter preset name")) {
                         TextField("Name", text: $state.newPresetName)
                         Button {
                             state.save()
-                            isPromtPresented = false
+                            isPromptPresented = false
                         }
                         label: { Text("Save") }
-                        Button { isPromtPresented = false }
+                        Button { isPromptPresented = false }
                         label: { Text("Cancel") }
                     }
                 }
@@ -144,7 +144,7 @@ extension AddTempTarget {
                 state.hbt = isEnabledArray.first?.hbt ?? 160
             }
             .navigationTitle("Enact Temp Target")
-            .navigationBarTitleDisplayMode(.automatic)
+            .navigationBarTitleDisplayMode(.inline)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
         }
 

+ 148 - 35
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -1,3 +1,5 @@
+
+import LoopKit
 import SwiftUI
 import Swinject
 
@@ -7,28 +9,63 @@ extension Bolus {
         @Injected() var apsManager: APSManager!
         @Injected() var broadcaster: Broadcaster!
         @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 insulinRecommended: 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 evBG: Int = 0
         @Published var insulin: Decimal = 0
-        @Published var target: 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 minDelta: Decimal = 0
         @Published var expectedDelta: 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
 
+        // 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() {
             setupInsulinRequired()
             broadcaster.register(SuggestionObserver.self, observer: self)
@@ -36,6 +73,11 @@ extension Bolus {
             percentage = settingsManager.settings.insulinReqPercentage
             threshold = provider.suggestion?.threshold ?? 0
             maxBolus = provider.pumpSettings().maxBolus
+            // added
+            fraction = settings.settings.overrideFactor
+            useCalc = settings.settings.useCalc
+            fattyMeals = settings.settings.fattyMeals
+            fattyMealFactor = settings.settings.fattyMealFactor
 
             if waitForSuggestionInitial {
                 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 apsManager
+                .roundBolus(amount: max(insulinCalculated, 0))
+        }
+
         func add() {
             guard amount > 0 else {
                 showModal(for: nil)
                 return
             }
 
-            let maxAmount = Double(min(amount, maxBolus))
+            let maxAmount = Double(min(amount, provider.pumpSettings().maxBolus))
 
             unlockmanager.unlock()
                 .sink { _ in } receiveValue: { [weak self] _ in
@@ -68,37 +176,10 @@ extension Bolus {
                 .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
-                    )
-                ]
-            )
-            showModal(for: nil)
-        }
-
         func setupInsulinRequired() {
             DispatchQueue.main.async {
                 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
                 if self.units == .mmolL {
                     conversion = 0.0555
@@ -108,6 +189,11 @@ extension Bolus {
                 self.insulin = self.provider.suggestion?.insulinForManualBolus ?? 0
                 self.target = self.provider.suggestion?.current_target ?? 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 {
                     self.insulinRecommended = self.insulin * (self.settingsManager.settings.insulinReqPercentage / 100)
@@ -124,6 +210,33 @@ extension Bolus {
 
                 self.insulinRecommended = self.apsManager
                     .roundBolus(amount: max(self.insulinRecommended, 0))
+
+                if self.useCalc {
+                    self.getDeltaBG()
+                    self.insulinCalculated = self.calculateInsulin()
+                }
+            }
+        }
+
+        func backToCarbsView(complexEntry: Bool, _ id: String) {
+            delete(deleteTwice: complexEntry, id: id)
+            showModal(for: .addCarbs(editMode: complexEntry))
+        }
+
+        func delete(deleteTwice: Bool, id: String) {
+            if deleteTwice {
+                // 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
+                )
             }
         }
     }

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

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

@@ -0,0 +1,495 @@
+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
+        @State private var keepForNextWiew: Bool = 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 fetch {
+                    Section {
+                        mealEntries
+                    } header: { Text("Meal Summary") }
+                }
+
+                Section {
+                    Button {
+                        let id_ = meal.first?.id ?? ""
+                        if fetch {
+                            keepForNextWiew = true
+                            state.backToCarbsView(complexEntry: fetch, id_)
+                        } else {
+                            state.showModal(for: .addCarbs(editMode: false))
+                        }
+                    }
+                    label: { Text(fetch ? "Edit Meal" : "Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
+                } header: { Text(!fetch ? "Meal Summary" : "") }
+
+                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") }
+
+                if state.amount > 0 {
+                    Section {
+                        Button {
+                            keepForNextWiew = true
+                            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
+                            )
+                    }
+                }
+                Section {
+                    Button {
+                        keepForNextWiew = true
+                        state.showModal(for: nil)
+                    }
+                    label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
+                }
+            }
+            .blur(radius: showInfo ? 3 : 0)
+            .navigationTitle("Enact Bolus")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(
+                leading: Button { state.hideModal() }
+                label: { Text("Close") }
+            )
+            .onAppear {
+                configureView {
+                    state.waitForSuggestionInitial = waitForSuggestion
+                    state.waitForSuggestion = waitForSuggestion
+                    state.insulinCalculated = state.calculateInsulin()
+                }
+            }
+            .onDisappear {
+                if fetch, hasFatOrProtein, !keepForNextWiew, state.useCalc {
+                    state.delete(deleteTwice: true, id: meal.first?.id ?? "")
+                } else if fetch, !keepForNextWiew, state.useCalc {
+                    state.delete(deleteTwice: false, id: meal.first?.id ?? "")
+                }
+            }
+            .popup(isPresented: showInfo) {
+                bolusInfoAlternativeCalculator
+            }
+        }
+
+        // Pop-up
+        var bolusInfoAlternativeCalculator: some View {
+            VStack {
+                VStack {
+                    VStack(spacing: Config.spacing) {
+                        HStack {
+                            Text("Calculations")
+                                .font(.title3).frame(maxWidth: .infinity, alignment: .center)
+                        }.padding(10)
+                        if fetch {
+                            mealEntries.padding()
+                            Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+                        }
+                        settings.padding()
+                    }
+                    Divider().frame(height: Config.dividerHeight) // .overlay(Config.overlayColour)
+                    insulinParts.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(" U")
+                                .foregroundColor(.secondary)
+                        }
+                    }.padding(.horizontal)
+                    Divider().frame(height: Config.dividerHeight)
+                    results.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)\(" U")")
+                        }
+                        .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))
+            )
+        }
+
+        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)
+        }
+
+        var mealEntries: some View {
+            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)
+                }
+            }
+        }
+
+        var settings: some View {
+            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)
+                    }
+                }
+            }
+        }
+
+        var insulinParts: some View {
+            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(" U")
+                        .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(" U")
+                        .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(" U").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(" U")
+                        .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(" U")
+                        .foregroundColor(.secondary)
+                }
+            }
+        }
+
+        var results: some View {
+            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(" U")
+                        .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(" U")
+                        .foregroundColor(.secondary)
+                }
+            }
+        }
+    }
+}

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

@@ -5,315 +5,22 @@ extension Bolus {
     struct RootView: 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")
-                            Spacer()
-                            Text(
-                                formatter
-                                    .string(from: state.insulinRecommended as NSNumber)! +
-                                    NSLocalizedString(" U", comment: "Insulin unit")
-                            ).foregroundColor((state.error && state.insulinRecommended > 0) ? .red : .secondary)
-                        }.contentShape(Rectangle())
-                            .onTapGesture {
-                                if state.error, state.insulinRecommended > 0 { displayError = true }
-                                else { state.amount = state.insulinRecommended }
-                            }
-                        HStack {
-                            Image(systemName: "info.bubble").symbolRenderingMode(.palette).foregroundStyle(
-                                .primary, .blue
-                            )
-                        }.onTapGesture {
-                            presentInfo.toggle()
-                        }
-                    }
-                }
-                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!") }
-                            .disabled(
-                                state.amount <= 0 || state.amount > state.maxBolus
-                            )
-                    }
-                    Section {
-                        if waitForSuggestion {
-                            Button { state.showModal(for: nil) }
-                            label: { Text("Continue without bolus") }
-                        } else {
-                            Button { isAddInsulinAlertPresented = true }
-                            label: { Text("Add insulin without actually bolusing") }
-                                .disabled(state.amount <= 0 || state.amount > state.maxBolus * 3)
-                        }
-                    }
-                    .alert(isPresented: $isAddInsulinAlertPresented) {
-                        let isOverMax = state.amount > state.maxBolus ? true : false
-                        let secondParagrap1 = "Add"
-                        let secondParagraph2 = " U"
-                        let secondParagraph3 = " without bolusing"
-                        let insulinAmount = formatter.string(from: state.amount as NSNumber)!
-
-                        // Actual alert
-                        return Alert(
-                            title: Text(
-                                isOverMax ? "Warning" : "Are you sure?"
-                            ),
-                            message:
-                            Text(
-                                isOverMax ? (
-                                    NSLocalizedString(
-                                        "\nAmount is more than your Max Bolus setting! \nAre you sure you want to add ",
-                                        comment: "Alert"
-                                    ) + insulinAmount +
-                                        NSLocalizedString(secondParagraph2, comment: "Insulin unit") +
-                                        NSLocalizedString(secondParagraph3, comment: "Add insulin without bolusing alert") + "?"
-                                ) :
-                                    NSLocalizedString(secondParagrap1, comment: "Add insulin without bolusing alert") +
-                                    " " +
-                                    insulinAmount +
-                                    NSLocalizedString(secondParagraph2, comment: "Insulin unit") +
-                                    NSLocalizedString(secondParagraph3, comment: "Add insulin without bolusing alert")
-                            ),
-                            primaryButton: .destructive(
-                                Text("Add"),
-                                action: {
-                                    state.addWithoutBolus()
-                                    isAddInsulinAlertPresented = false
-                                }
-                            ),
-                            secondaryButton: .cancel()
-                        )
-                    }
-                }
-            }
-            .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(.automatic)
-            .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 {
     @Binding var isAnimating: Bool
     let style: UIActivityIndicatorView.Style

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

@@ -0,0 +1,355 @@
+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
+        @State private var keepForNextWiew: Bool = false
+
+        @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 fractionDigits: Int {
+            if state.units == .mmolL {
+                return 1
+            } else { return 0 }
+        }
+
+        var body: some View {
+            Form {
+                if fetch {
+                    Section {
+                        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)
+                            }
+                        }
+                    } header: { Text("Meal Summary") }
+                }
+
+                Section {
+                    Button {
+                        let id_ = meal.first?.id ?? ""
+                        if fetch {
+                            keepForNextWiew = true
+                            state.backToCarbsView(complexEntry: fetch, id_)
+                        } else {
+                            state.showModal(for: .addCarbs(editMode: false))
+                        }
+                    }
+                    label: { Text(fetch ? "Edit Meal" : "Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
+                } header: { Text(!fetch ? "Meal Summary" : "") }
+
+                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())
+
+                        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 Summary") }
+
+                if !state.waitForSuggestion {
+                    if state.amount > 0 {
+                        Section {
+                            Button {
+                                keepForNextWiew = true
+                                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 {
+                                keepForNextWiew = true
+                                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
+                }
+            }
+
+            .onDisappear {
+                if fetch, hasFatOrProtein, !keepForNextWiew, !state.useCalc {
+                    state.delete(deleteTwice: true, id: meal.first?.id ?? "")
+                } else if fetch, !keepForNextWiew, !state.useCalc {
+                    state.delete(deleteTwice: false, id: meal.first?.id ?? "")
+                }
+            }
+
+            .navigationTitle("Enact Bolus")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
+                bolusInfo
+            }
+        }
+
+        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)
+        }
+
+        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)
+        }
+    }
+}

+ 15 - 8
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -67,6 +67,7 @@ enum DataTable {
         let fpuID: String?
         let note: String?
         let isSMB: Bool?
+        let isExternal: Bool?
 
         private var numberFormatter: NumberFormatter {
             let formatter = NumberFormatter()
@@ -94,7 +95,8 @@ enum DataTable {
             isFPU: Bool? = nil,
             fpuID: String? = nil,
             note: String? = nil,
-            isSMB: Bool? = nil
+            isSMB: Bool? = nil,
+            isExternal: Bool? = nil
         ) {
             self.units = units
             self.type = type
@@ -108,6 +110,7 @@ enum DataTable {
             self.fpuID = fpuID
             self.note = note
             self.isSMB = isSMB
+            self.isExternal = isExternal
         }
 
         static func == (lhs: Treatment, rhs: Treatment) -> Bool {
@@ -135,12 +138,16 @@ enum DataTable {
                 return numberFormatter
                     .string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carb equilvalents")
             case .bolus:
+                var bolusText = " "
+                if isSMB ?? false {}
+                else if isExternal ?? false {
+                    bolusText += NSLocalizedString("External", comment: "External Insulin")
+                } else {
+                    bolusText += NSLocalizedString("Manual", comment: "Manual Bolus")
+                }
+
                 return numberFormatter
-                    .string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") +
-                    (
-                        (isSMB ?? false) ? " " + NSLocalizedString("Automatic", comment: "Automatic delivered treatments") : " " +
-                            NSLocalizedString("Manual", comment: "Manual Bolus")
-                    )
+                    .string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") + bolusText
             case .tempBasal:
                 return numberFormatter
                     .string(from: amount as NSNumber)! + NSLocalizedString(" U/hr", comment: "Unit insulin per hour")
@@ -172,9 +179,9 @@ enum DataTable {
             case .fpus:
                 return .orange.opacity(0.5)
             case .bolus:
-                return .insulin
+                return Color.insulin
             case .tempBasal:
-                return Color.insulin.opacity(0.5)
+                return Color.insulin.opacity(0.4)
             case .resume,
                  .suspend,
                  .tempTarget:

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

@@ -13,6 +13,12 @@ extension DataTable {
             pumpHistoryStorage.recent()
         }
 
+        func pumpSettings() -> PumpSettings {
+            storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+                ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+                ?? PumpSettings(insulinActionCurve: 6, maxBolus: 10, maxBasal: 2)
+        }
+
         func tempTargets() -> [TempTarget] {
             tempTargetsStorage.recent()
         }
@@ -27,7 +33,7 @@ extension DataTable {
 
         func deleteCarbs(_ treatement: Treatment) {
             nightscoutManager.deleteCarbs(
-                at: treatement.date,
+                at: treatement.id,
                 isFPU: treatement.isFPU,
                 fpuID: treatement.fpuID,
                 syncID: treatement.id

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

@@ -6,6 +6,7 @@ extension DataTable {
         @Injected() var broadcaster: Broadcaster!
         @Injected() var unlockmanager: UnlockManager!
         @Injected() private var storage: FileStorage!
+        @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var healthKitManager: HealthKitManager!
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
@@ -13,12 +14,16 @@ extension DataTable {
         @Published var mode: Mode = .treatments
         @Published var treatments: [Treatment] = []
         @Published var glucose: [Glucose] = []
-        @Published var manualGlcuose: Decimal = 0
+        @Published var manualGlucose: Decimal = 0
+        @Published var maxBolus: Decimal = 0
+        @Published var externalInsulinAmount: Decimal = 0
+        @Published var externalInsulinDate = Date()
 
         var units: GlucoseUnits = .mmolL
 
         override func subscribe() {
             units = settingsManager.settings.units
+            maxBolus = provider.pumpSettings().maxBolus
             setupTreatments()
             setupGlucose()
             broadcaster.register(SettingsObserver.self, observer: self)
@@ -35,7 +40,7 @@ extension DataTable {
                 let carbs = self.provider.carbs()
                     .filter { !($0.isFPU ?? false) }
                     .map {
-                        if let id = $0.id {
+                        if let id = $0.collectionID {
                             return Treatment(
                                 units: units,
                                 type: .carbs,
@@ -57,7 +62,7 @@ extension DataTable {
                             type: .fpus,
                             date: $0.createdAt,
                             amount: $0.carbs,
-                            id: $0.id,
+                            id: $0.collectionID,
                             isFPU: $0.isFPU,
                             fpuID: $0.fpuID,
                             note: $0.note
@@ -73,7 +78,8 @@ extension DataTable {
                             date: $0.timestamp,
                             amount: $0.amount,
                             idPumpEvent: $0.id,
-                            isSMB: $0.isSMB
+                            isSMB: $0.isSMB,
+                            isExternal: $0.isExternal
                         )
                     }
 
@@ -148,7 +154,6 @@ extension DataTable {
         func deleteGlucose(at index: Int) {
             let id = glucose[index].id
             provider.deleteGlucose(id: id)
-            // CoreData
             let fetchRequest: NSFetchRequest<NSFetchRequestResult>
             fetchRequest = NSFetchRequest(entityName: "Readings")
             fetchRequest.predicate = NSPredicate(format: "id == %@", id)
@@ -165,14 +170,14 @@ extension DataTable {
                     )
                 }
             } catch { /* To do: handle any thrown errors. */ }
-            // Manual Glucose
+            // Deletes Manual Glucose
             if (glucose[index].glucose.type ?? "") == GlucoseType.manual.rawValue {
                 provider.deleteManualGlucose(date: glucose[index].glucose.dateString)
             }
         }
 
         func addManualGlucose() {
-            let glucose = units == .mmolL ? manualGlcuose.asMgdL : manualGlcuose
+            let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let now = Date()
             let id = UUID().uuidString
 
@@ -192,7 +197,40 @@ extension DataTable {
             // Save to Health
             var saveToHealth = [BloodGlucose]()
             saveToHealth.append(saveToJSON)
-            healthKitManager.saveIfNeeded(bloodGlucose: saveToHealth)
+        }
+
+        func addExternalInsulin() {
+            guard externalInsulinAmount > 0 else {
+                showModal(for: nil)
+                return
+            }
+
+            externalInsulinAmount = min(externalInsulinAmount, maxBolus * 3) // Allow for 3 * Max Bolus for external insulin
+            unlockmanager.unlock()
+                .sink { _ in } receiveValue: { [weak self] _ in
+                    guard let self = self else { return }
+                    pumpHistoryStorage.storeEvents(
+                        [
+                            PumpHistoryEvent(
+                                id: UUID().uuidString,
+                                type: .bolus,
+                                timestamp: externalInsulinDate,
+                                amount: externalInsulinAmount,
+                                duration: nil,
+                                durationMin: nil,
+                                rate: nil,
+                                temp: nil,
+                                carbInput: nil,
+                                isExternal: true
+                            )
+                        ]
+                    )
+                    debug(.default, "External insulin saved to pumphistory.json")
+
+                    // Reset amount to 0 for next entry.
+                    externalInsulinAmount = 0
+                }
+                .store(in: &lifetime)
         }
     }
 }

+ 182 - 63
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -11,12 +11,20 @@ extension DataTable {
         @State private var removeCarbsAlert: Alert?
         @State private var isRemoveInsulinAlertPresented = false
         @State private var removeInsulinAlert: Alert?
-        @State private var newGlucose = false
-        @State private var isLayered = false
-        @FocusState private var isFocused: Bool
+        @State private var showExternalInsulin: Bool = false
+        @State private var showFutureEntries: Bool = false // default to hide future entries
+        @State private var showManualGlucose: Bool = false
+        @State private var isAmountUnconfirmed: Bool = true
 
         @Environment(\.colorScheme) var colorScheme
 
+        private var insulinFormatter: NumberFormatter {
+            let formatter = NumberFormatter()
+            formatter.numberStyle = .decimal
+            formatter.maximumFractionDigits = 2
+            return formatter
+        }
+
         private var glucoseFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
@@ -52,87 +60,132 @@ extension DataTable {
                 }
             }
             .onAppear(perform: configureView)
-            .navigationTitle(isLayered ? "" : "History")
-            .blur(radius: isLayered ? 3.0 : 0)
-            .navigationBarTitleDisplayMode(.automatic)
-            .navigationBarItems(leading: Button(isLayered ? "" : "Close", action: state.hideModal))
-            .popup(isPresented: newGlucose, alignment: .center, direction: .top) {
-                addGlucose
+            .navigationTitle("History")
+            .navigationBarTitleDisplayMode(.inline)
+            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .sheet(isPresented: $showManualGlucose) {
+                addGlucoseView
+            }
+            .sheet(isPresented: $showExternalInsulin, onDismiss: { if isAmountUnconfirmed { state.externalInsulinAmount = 0
+                state.externalInsulinDate = Date() } }) {
+                addExternalInsulinView
             }
         }
 
         private var treatmentsList: some View {
             List {
-                ForEach(state.treatments) { item in
-                    treatmentView(item)
+                HStack {
+                    Button(action: { showExternalInsulin = true
+                        state.externalInsulinDate = Date() }, label: {
+                        HStack {
+                            Image(systemName: "syringe")
+                            Text("Add")
+                                .foregroundColor(Color.secondary)
+                                .font(.caption)
+                        }.frame(maxWidth: .infinity, alignment: .leading)
+                    }).buttonStyle(.borderless)
+
+                    Spacer()
+
+                    Button(action: { showFutureEntries.toggle() }, label: {
+                        HStack {
+                            Text(showFutureEntries ? "Hide Future" : "Show Future")
+                                .foregroundColor(Color.secondary)
+                                .font(.caption)
+                            Image(systemName: showFutureEntries ? "calendar.badge.minus" : "calendar.badge.plus")
+                        }.frame(maxWidth: .infinity, alignment: .trailing)
+                    }).buttonStyle(.borderless)
+                }
+                if !state.treatments.isEmpty {
+                    if !showFutureEntries {
+                        ForEach(state.treatments.filter { item in
+                            item.date <= Date()
+                        }) { item in
+                            treatmentView(item)
+                        }
+                    } else {
+                        ForEach(state.treatments) { item in
+                            treatmentView(item)
+                        }
+                    }
+                } else {
+                    HStack {
+                        Text("No data.")
+                    }
                 }
             }
         }
 
         private var glucoseList: some View {
             List {
-                Button {
-                    newGlucose = true
-                    isFocused = true
-                    isLayered.toggle()
+                HStack {
+                    Button(
+                        action: { showManualGlucose = true
+                            state.manualGlucose = 0 },
+                        label: { Image(systemName: "plus.circle.fill").foregroundStyle(.secondary)
+                        }
+                    ).buttonStyle(.borderless)
+                    Text(state.units.rawValue).foregroundStyle(.secondary)
+                    Spacer()
+                    Text("Time").foregroundStyle(.secondary)
+                }
+                if !state.glucose.isEmpty {
+                    ForEach(state.glucose) { item in
+                        glucoseView(item, isManual: item.glucose)
+                    }
+                    .onDelete(perform: deleteGlucose)
+                } else {
+                    HStack {
+                        Text("No data.")
+                    }
                 }
-                label: { Text("Add") }.frame(maxWidth: .infinity, alignment: .trailing)
-                    .padding(.trailing, 20)
-
-                ForEach(state.glucose) { item in
-                    glucoseView(item, isManual: item.glucose)
-                }.onDelete(perform: deleteGlucose)
             }
         }
 
-        private var addGlucose: some View {
-            VStack {
-                Form {
-                    Section {
-                        HStack {
-                            Text("Glucose").font(.custom("popup", fixedSize: 18))
-                            DecimalTextField(" ... ", value: $state.manualGlcuose, formatter: glucoseFormatter)
-                                .focused($isFocused).font(.custom("glucose", fixedSize: 22))
-                            Text(state.units.rawValue).foregroundStyle(.secondary)
-                        }
-                    }
-                    header: {
-                        Text("Blood Glucose Test").foregroundColor(.secondary).font(.custom("popupHeader", fixedSize: 12))
-                            .padding(.top)
-                    }
-                    HStack {
-                        Button {
-                            newGlucose = false
-                            isLayered = false
+        var addGlucoseView: some View {
+            NavigationView {
+                VStack {
+                    Form {
+                        Section {
+                            HStack {
+                                Text("New Glucose")
+                                DecimalTextField(
+                                    " ... ",
+                                    value: $state.manualGlucose,
+                                    formatter: glucoseFormatter,
+                                    autofocus: true,
+                                    cleanInput: true
+                                )
+                                Text(state.units.rawValue).foregroundStyle(.secondary)
+                            }
                         }
-                        label: { Text("Cancel").foregroundColor(.red) }
-                            .frame(maxWidth: .infinity, alignment: .leading)
-                        Spacer()
-                        Button {
-                            state.addManualGlucose()
-                            newGlucose = false
-                            isLayered = false
+
+                        Section {
+                            HStack {
+                                let limitLow: Decimal = state.units == .mmolL ? 0.8 : 14
+                                let limitHigh: Decimal = state.units == .mmolL ? 40 : 720
+                                Button {
+                                    state.addManualGlucose()
+                                    isAmountUnconfirmed = false
+                                    showManualGlucose = false
+                                }
+                                label: { Text("Save") }
+                                    .frame(maxWidth: .infinity, alignment: .center)
+                                    .disabled(state.manualGlucose < limitLow || state.manualGlucose > limitHigh)
+                            }
                         }
-                        label: { Text("Save") }
-                            .frame(maxWidth: .infinity, alignment: .trailing)
-                            .disabled(state.manualGlcuose <= 0)
                     }
-                    .buttonStyle(BorderlessButtonStyle())
-                    .font(.custom("popupButtons", fixedSize: 16))
                 }
+                .onAppear(perform: configureView)
+                .navigationTitle("Add Glucose")
+                .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: Button("Close", action: { showManualGlucose = false }))
             }
-            .frame(maxHeight: 220)
-            .background(
-                RoundedRectangle(cornerRadius: 8, style: .continuous)
-                    .fill(Color(.tertiarySystemBackground))
-            ).border(.gray).shadow(radius: 40)
         }
 
         @ViewBuilder private func treatmentView(_ item: Treatment) -> some View {
             HStack {
                 Image(systemName: "circle.fill").foregroundColor(item.color)
-                Text(dateFormatter.string(from: item.date))
-                    .moveDisabled(true)
                 Text((item.isSMB ?? false) ? "SMB" : item.type.name)
                 Text(item.amountText).foregroundColor(.secondary)
 
@@ -155,7 +208,8 @@ extension DataTable {
                                 message: Text(item.amountText),
                                 primaryButton: .destructive(
                                     Text("Delete"),
-                                    action: { state.deleteCarbs(item) }
+                                    action: {
+                                        state.deleteCarbs(item) }
                                 ),
                                 secondaryButton: .cancel()
                             )
@@ -209,25 +263,90 @@ extension DataTable {
                             removeInsulinAlert!
                         }
                 }
+                Spacer()
+                Text(dateFormatter.string(from: item.date))
+                    .moveDisabled(true)
+            }
+        }
+
+        var addExternalInsulinView: some View {
+            NavigationView {
+                VStack {
+                    Form {
+                        Section {
+                            HStack {
+                                Text("Amount")
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.externalInsulinAmount,
+                                    formatter: insulinFormatter,
+                                    autofocus: true,
+                                    cleanInput: true
+                                )
+                                Text("U").foregroundColor(.secondary)
+                            }
+                        }
+
+                        Section {
+                            DatePicker("Date", selection: $state.externalInsulinDate, in: ...Date())
+                        }
+
+                        let amountWarningCondition = (state.externalInsulinAmount > state.maxBolus)
+
+                        Section {
+                            HStack {
+                                Button {
+                                    state.addExternalInsulin()
+                                    isAmountUnconfirmed = false
+                                    showExternalInsulin = false
+                                }
+                                label: {
+                                    Text("Log external insulin")
+                                }
+                                .foregroundColor(amountWarningCondition ? Color.white : Color.accentColor)
+                                .frame(maxWidth: .infinity, alignment: .center)
+                                .disabled(
+                                    state.externalInsulinAmount <= 0 || state.externalInsulinAmount > state.maxBolus * 3
+                                )
+                            }
+                        }
+                        header: {
+                            if amountWarningCondition
+                            {
+                                Text("⚠️ Warning! The entered insulin amount is greater than your Max Bolus setting!")
+                            }
+                        }
+                        .listRowBackground(
+                            amountWarningCondition ? Color
+                                .red : colorScheme == .dark ? Color(UIColor.secondarySystemBackground) : Color.white
+                        )
+                    }
+                }
+                .onAppear(perform: configureView)
+                .navigationTitle("External Insulin")
+                .navigationBarTitleDisplayMode(.inline)
+                .navigationBarItems(leading: Button("Close", action: { showExternalInsulin = false
+                    state.externalInsulinAmount = 0 }))
             }
         }
 
         @ViewBuilder private func glucoseView(_ item: Glucose, isManual: BloodGlucose) -> some View {
             VStack(alignment: .leading, spacing: 4) {
                 HStack {
-                    Text(dateFormatter.string(from: item.glucose.dateString))
-                    Spacer()
                     Text(item.glucose.glucose.map {
                         glucoseFormatter.string(from: Double(
                             state.units == .mmolL ? $0.asMmolL : Decimal($0)
                         ) as NSNumber)!
                     } ?? "--")
-                    Text(state.units.rawValue)
                     if isManual.type == GlucoseType.manual.rawValue {
                         Image(systemName: "drop.fill").symbolRenderingMode(.monochrome).foregroundStyle(.red)
                     } else {
                         Text(item.glucose.direction?.symbol ?? "--")
                     }
+                    Spacer()
+
+                    Text(dateFormatter.string(from: item.glucose.dateString))
                 }
             }
         }

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

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

+ 7 - 0
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -9,6 +9,7 @@ extension Home {
         @Injected() var pumpHistoryStorage: PumpHistoryStorage!
         @Injected() var tempTargetsStorage: TempTargetsStorage!
         @Injected() var carbsStorage: CarbsStorage!
+        @Injected() var announcementStorage: AnnouncementsStorage!
 
         var suggestion: Suggestion? {
             storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
@@ -57,6 +58,12 @@ extension Home {
             }
         }
 
+        func announcement(_ hours: Int) -> [Announcement] {
+            announcementStorage.validate().filter {
+                $0.createdAt.addingTimeInterval(hours.hours.timeInterval) > Date()
+            }
+        }
+
         func pumpSettings() -> PumpSettings {
             storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))

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

@@ -13,6 +13,7 @@ extension Home {
         private(set) var filteredHours = 24
         @Published var glucose: [BloodGlucose] = []
         @Published var isManual: [BloodGlucose] = []
+        @Published var announcement: [Announcement] = []
         @Published var suggestion: Suggestion?
         @Published var uploadStats = false
         @Published var enactedSuggestion: Suggestion?
@@ -73,6 +74,7 @@ extension Home {
             setupCarbs()
             setupBattery()
             setupReservoir()
+            setupAnnouncements()
 
             suggestion = provider.suggestion
             uploadStats = settingsManager.settings.uploadStats
@@ -194,7 +196,7 @@ extension Home {
         }
 
         func addCarbs() {
-            showModal(for: .addCarbs)
+            showModal(for: .addCarbs(editMode: false))
         }
 
         func runLoop() {
@@ -308,6 +310,13 @@ extension Home {
             }
         }
 
+        private func setupAnnouncements() {
+            DispatchQueue.main.async { [weak self] in
+                guard let self = self else { return }
+                self.announcement = self.provider.announcement(self.filteredHours)
+            }
+        }
+
         private func setStatusTitle() {
             guard let suggestion = suggestion else {
                 statusTitle = "No suggestion"
@@ -423,6 +432,7 @@ extension Home.StateModel:
         setupBasals()
         setupBoluses()
         setupSuspensions()
+        setupAnnouncements()
     }
 
     func pumpSettingsDidChange(_: PumpSettings) {

+ 77 - 0
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -14,6 +14,12 @@ struct DotInfo {
     let value: Decimal
 }
 
+struct AnnouncementDot {
+    let rect: CGRect
+    let value: Decimal
+    let note: String
+}
+
 typealias GlucoseYRange = (minValue: Int, minY: CGFloat, maxValue: Int, maxY: CGFloat)
 
 struct MainChartView: View {
@@ -33,6 +39,19 @@ struct MainChartView: View {
         static let fpuSize: CGFloat = 5
         static let carbsScale: CGFloat = 0.3
         static let fpuScale: CGFloat = 1
+        static let announcementSize: CGFloat = 8
+        static let announcementScale: CGFloat = 2.5
+        static let owlSeize: CGFloat = 25
+        static let owlOffset: CGFloat = 80
+    }
+
+    private enum Command {
+        static let open = "🔴"
+        static let closed = "🟢"
+        static let suspend = "❌"
+        static let resume = "✅"
+        static let tempbasal = "basal"
+        static let bolus = "💧"
     }
 
     @Binding var glucose: [BloodGlucose]
@@ -41,6 +60,7 @@ struct MainChartView: View {
     @Binding var tempBasals: [PumpHistoryEvent]
     @Binding var boluses: [PumpHistoryEvent]
     @Binding var suspensions: [PumpHistoryEvent]
+    @Binding var announcement: [Announcement]
     @Binding var hours: Int
     @Binding var maxBasal: Decimal
     @Binding var autotunedBasalProfile: [BasalProfileEntry]
@@ -60,6 +80,8 @@ struct MainChartView: View {
     @State var didAppearTrigger = false
     @State private var glucoseDots: [CGRect] = []
     @State private var manualGlucoseDots: [CGRect] = []
+    @State private var announcementDots: [AnnouncementDot] = []
+    @State private var announcementPath = Path()
     @State private var manualGlucoseDotsCenter: [CGRect] = []
     @State private var unSmoothedGlucoseDots: [CGRect] = []
     @State private var predictionDots: [PredictionType: [CGRect]] = [:]
@@ -277,6 +299,7 @@ struct MainChartView: View {
                     glucoseView(fullSize: fullSize)
                     manualGlucoseView(fullSize: fullSize)
                     manualGlucoseCenterView(fullSize: fullSize)
+                    announcementView(fullSize: fullSize)
                     predictionsView(fullSize: fullSize)
                 }
                 timeLabelsView(fullSize: fullSize)
@@ -367,6 +390,35 @@ struct MainChartView: View {
         }
     }
 
+    private func announcementView(fullSize: CGSize) -> some View {
+        ZStack {
+            ForEach(announcementDots, id: \.rect.minX) { info -> AnyView in
+                let position = CGPoint(x: info.rect.midX + 5, y: info.rect.maxY - Config.owlOffset)
+                let type: String =
+                    info.note.contains("true") ?
+                    Command.open :
+                    info.note.contains("false") ?
+                    Command.closed :
+                    info.note.contains("suspend") ?
+                    Command.suspend :
+                    info.note.contains("resume") ?
+                    Command.resume :
+                    info.note.contains("tempbasal") ?
+                    Command.tempbasal : Command.bolus
+                VStack {
+                    Text(type).font(.caption2).foregroundStyle(.orange)
+                    Image("owl").resizable().frame(maxWidth: Config.owlSeize, maxHeight: Config.owlSeize).scaledToFill()
+                }.position(position).asAny()
+            }
+        }
+        .onChange(of: announcement) { _ in
+            calculateAnnouncementDots(fullSize: fullSize)
+        }
+        .onChange(of: didAppearTrigger) { _ in
+            calculateAnnouncementDots(fullSize: fullSize)
+        }
+    }
+
     private func manualGlucoseCenterView(fullSize: CGSize) -> some View {
         Path { path in
             for rect in manualGlucoseDotsCenter {
@@ -530,6 +582,7 @@ extension MainChartView {
         calculateGlucoseDots(fullSize: fullSize)
         calculateManualGlucoseDots(fullSize: fullSize)
         calculateManualGlucoseDotsCenter(fullSize: fullSize)
+        calculateAnnouncementDots(fullSize: fullSize)
         calculateUnSmoothedGlucoseDots(fullSize: fullSize)
         calculateBolusDots(fullSize: fullSize)
         calculateCarbsDots(fullSize: fullSize)
@@ -587,6 +640,30 @@ extension MainChartView {
         }
     }
 
+    private func calculateAnnouncementDots(fullSize: CGSize) {
+        calculationQueue.async {
+            let dots = announcement.map { value -> AnnouncementDot in
+                let center = timeToInterpolatedPoint(value.createdAt.timeIntervalSince1970, fullSize: fullSize)
+                let size = Config.announcementSize * Config.announcementScale
+                let rect = CGRect(x: center.x - size / 2, y: center.y - size / 2, width: size, height: size)
+                let note = value.notes
+                return AnnouncementDot(rect: rect, value: 10, note: note)
+            }
+            let path = Path { path in
+                for dot in dots {
+                    path.addEllipse(in: dot.rect)
+                }
+            }
+            let range = self.getGlucoseYRange(fullSize: fullSize)
+
+            DispatchQueue.main.async {
+                glucoseYRange = range
+                announcementDots = dots
+                announcementPath = path
+            }
+        }
+    }
+
     private func calculateUnSmoothedGlucoseDots(fullSize: CGSize) {
         calculationQueue.async {
             let dots = glucose.concurrentMap { value -> CGRect in

+ 11 - 5
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -95,7 +95,7 @@ extension Home {
             .frame(maxWidth: .infinity)
             .padding(.top, 10 + geo.safeAreaInsets.top)
             .padding(.bottom, 10)
-            .background(Color.gray.opacity(0.2))
+            .background(Color.gray.opacity(0.3))
         }
 
         var cobIobView: some View {
@@ -389,6 +389,7 @@ extension Home {
                     tempBasals: $state.tempBasals,
                     boluses: $state.boluses,
                     suspensions: $state.suspensions,
+                    announcement: $state.announcement,
                     hours: .constant(state.filteredHours),
                     maxBasal: $state.maxBasal,
                     autotunedBasalProfile: $state.autotunedBasalProfile,
@@ -414,7 +415,7 @@ extension Home {
             let colour: Color = colorScheme == .dark ? .black : .white
             // Rectangle().fill(colour).frame(maxHeight: 1)
             ZStack {
-                Rectangle().fill(Color.gray.opacity(0.2)).frame(maxHeight: 40)
+                Rectangle().fill(Color.gray.opacity(0.3)).frame(maxHeight: 40)
                 let cancel = fetchedPercent.first?.enabled ?? false
                 HStack(spacing: cancel ? 25 : 15) {
                     Text(selectedProfile().name).foregroundColor(.secondary)
@@ -477,10 +478,10 @@ extension Home {
 
         @ViewBuilder private func bottomPanel(_ geo: GeometryProxy) -> some View {
             ZStack {
-                Rectangle().fill(Color.gray.opacity(0.2)).frame(height: 50 + geo.safeAreaInsets.bottom)
+                Rectangle().fill(Color.gray.opacity(0.3)).frame(height: 50 + geo.safeAreaInsets.bottom)
 
                 HStack {
-                    Button { state.showModal(for: .addCarbs) }
+                    Button { state.showModal(for: .addCarbs(editMode: false)) }
                     label: {
                         ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
                             Image("carbs")
@@ -510,7 +511,12 @@ extension Home {
                     .foregroundColor(.loopGreen)
                     .buttonStyle(.borderless)
                     Spacer()
-                    Button { state.showModal(for: .bolus(waitForSuggestion: false)) }
+                    Button {
+                        state.showModal(for: .bolus(
+                            waitForSuggestion: true,
+                            fetch: false
+                        ))
+                    }
                     label: {
                         Image("bolus")
                             .renderingMode(.template)

+ 3 - 1
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -266,7 +266,9 @@ extension NightscoutConfig {
                                 debug(.service, "Settings have been imported and the Basals saved to pump!")
                                 // DIA. Save if changed.
                                 let dia = fetchedProfile.dia
-                                if dia != self.dia, dia <= 0 {
+                                print("dia: " + dia.description)
+                                print("pump dia: " + self.dia.description)
+                                if dia != self.dia, dia >= 0 {
                                     let file = PumpSettings(
                                         insulinActionCurve: dia,
                                         maxBolus: self.maxBolus,

+ 10 - 4
FreeAPS/Sources/Modules/OverrideProfilesConfig/OverrideProfilesStateModel.swift

@@ -24,13 +24,15 @@ extension OverrideProfilesConfig {
         @Published var end: Decimal = 23
         @Published var smbMinutes: Decimal = 0
         @Published var uamMinutes: Decimal = 0
+        @Published var defaultSmbMinutes: Decimal = 0
+        @Published var defaultUamMinutes: Decimal = 0
 
         var units: GlucoseUnits = .mmolL
 
         override func subscribe() {
             units = settingsManager.settings.units
-            smbMinutes = settingsManager.preferences.maxSMBBasalMinutes
-            uamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
+            defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
+            defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
             presets = [OverridePresets(context: coredataContext)]
         }
 
@@ -151,8 +153,8 @@ extension OverrideProfilesConfig {
                         saveOverride.end = profile.end
                     } else { saveOverride.smbIsAlwaysOff = false }
 
-                    saveOverride.smbMinutes = smbMinutes as NSDecimalNumber
-                    saveOverride.uamMinutes = uamMinutes as NSDecimalNumber
+                    saveOverride.smbMinutes = (profile.smbMinutes ?? 0) as NSDecimalNumber
+                    saveOverride.uamMinutes = (profile.uamMinutes ?? 0) as NSDecimalNumber
                 }
                 try? self.coredataContext.save()
             }
@@ -221,6 +223,8 @@ extension OverrideProfilesConfig {
                     override_target = false
                     smbIsOff = false
                     advancedSettings = false
+                    smbMinutes = defaultSmbMinutes
+                    uamMinutes = defaultUamMinutes
                 }
             }
         }
@@ -240,6 +244,8 @@ extension OverrideProfilesConfig {
                 profiles.date = Date()
                 try? self.coredataContext.save()
             }
+            smbMinutes = defaultSmbMinutes
+            uamMinutes = defaultUamMinutes
         }
     }
 }

+ 15 - 13
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -159,9 +159,8 @@ extension OverrideProfilesConfig {
                         }
                         HStack {
                             Text("SMB Minutes")
-                            let minutes = state.settingsManager.preferences.maxSMBBasalMinutes
                             DecimalTextField(
-                                minutes.formatted(),
+                                "0",
                                 value: $state.smbMinutes,
                                 formatter: formatter,
                                 cleanInput: false
@@ -170,9 +169,8 @@ extension OverrideProfilesConfig {
                         }
                         HStack {
                             Text("UAM SMB Minutes")
-                            let uam_minutes = state.settingsManager.preferences.maxUAMSMBBasalMinutes
                             DecimalTextField(
-                                uam_minutes.formatted(),
+                                "0",
                                 value: $state.uamMinutes,
                                 formatter: formatter,
                                 cleanInput: false
@@ -217,10 +215,7 @@ extension OverrideProfilesConfig {
                                     comment: ""
                                 )
                         }
-                        .disabled(
-                            (state.percentage == 100 && !state.override_target && !state.smbIsOff) ||
-                                (!state._indefinite && state.duration == 0) || (state.override_target && state.target == 0)
-                        )
+                        .disabled(unChanged())
                         .buttonStyle(BorderlessButtonStyle())
                         .font(.callout)
                         .controlSize(.mini)
@@ -248,12 +243,8 @@ extension OverrideProfilesConfig {
                             .frame(maxWidth: .infinity, alignment: .trailing)
                             .buttonStyle(BorderlessButtonStyle())
                             .controlSize(.mini)
-                            .disabled(
-                                (state.percentage == 100 && !state.override_target && !state.smbIsOff) ||
-                                    (!state._indefinite && state.duration == 0) || (state.override_target && state.target == 0)
-                            )
+                            .disabled(unChanged())
                     }
-
                     .sheet(isPresented: $isSheetPresented) {
                         presetPopover
                     }
@@ -336,6 +327,17 @@ extension OverrideProfilesConfig {
             }
         }
 
+        private func unChanged() -> Bool {
+            let isChanged = (state.percentage == 100 && !state.override_target && !state.smbIsOff && !state.advancedSettings) ||
+                (!state._indefinite && state.duration == 0) || (state.override_target && state.target == 0) ||
+                (
+                    state.percentage == 100 && !state.override_target && !state.smbIsOff && state.isf && state.cr && state
+                        .smbMinutes == state.defaultSmbMinutes && state.uamMinutes == state.defaultUamMinutes
+                )
+
+            return isChanged
+        }
+
         private func removeProfile(at offsets: IndexSet) {
             for index in offsets {
                 let language = fetchedProfiles[index]

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

@@ -8,9 +8,11 @@ extension PreferencesEditor {
         @Published var insulinReqPercentage: Decimal = 70
         @Published var skipBolusScreenAfterCarbs = false
         @Published var sections: [FieldSection] = []
+        @Published var useAlternativeBolusCalc: Bool = false
 
         override func subscribe() {
             preferences = provider.preferences
+            useAlternativeBolusCalc = settingsManager.settings.useCalc
             subscribeSetting(\.allowAnnouncements, on: $allowAnnouncements) { allowAnnouncements = $0 }
             subscribeSetting(\.insulinReqPercentage, on: $insulinReqPercentage) { insulinReqPercentage = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
@@ -23,12 +25,6 @@ extension PreferencesEditor {
 
             let mainFields = [
                 Field(
-                    displayName: NSLocalizedString("Insulin curve", comment: "Insulin curve"),
-                    type: .insulinCurve(keypath: \.curve),
-                    infoText: "Insulin curve info",
-                    settable: self
-                ),
-                Field(
                     displayName: NSLocalizedString("Max IOB", comment: "Max IOB"),
                     type: .decimal(keypath: \.maxIOB),
                     infoText: NSLocalizedString(

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

@@ -30,9 +30,11 @@ extension PreferencesEditor {
 
                     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)

+ 23 - 5
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -25,7 +25,26 @@ extension Settings {
 
             versionNumber = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
 
-            branch = Bundle.main.infoDictionary?["BuildBranch"] as? String ?? "Unknown"
+            // Read branch information from the branch.txt instead of infoDictionary
+            if let branchFileURL = Bundle.main.url(forResource: "branch", withExtension: "txt"),
+               let branchFileContent = try? String(contentsOf: branchFileURL)
+            {
+                let lines = branchFileContent.components(separatedBy: .newlines)
+                for line in lines {
+                    let components = line.components(separatedBy: "=")
+                    if components.count == 2 {
+                        let key = components[0].trimmingCharacters(in: .whitespaces)
+                        let value = components[1].trimmingCharacters(in: .whitespaces)
+
+                        if key == "BRANCH" {
+                            branch = value
+                            break
+                        }
+                    }
+                }
+            } else {
+                branch = "Unknown"
+            }
 
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
 
@@ -46,13 +65,12 @@ extension Settings {
             return items
         }
 
-        func uploadProfileAndSettings() {
-            NSLog("SettingsState Upload Profile")
-            nightscoutManager.uploadProfileAndSettings()
+        func uploadProfileAndSettings(_ force: Bool) {
+            NSLog("SettingsState Upload Profile and Settings")
+            nightscoutManager.uploadProfileAndSettings(force)
         }
 
         func hideSettingsModal() {
-            nightscoutManager.uploadProfileAndSettings()
             hideModal()
         }
     }

+ 5 - 3
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -12,7 +12,7 @@ extension Settings {
             Form {
                 Section(
                     header: Text(
-                        "iAPS v\(state.versionNumber) - \(state.buildNumber) \nBranch: \(state.branch) \(state.copyrightNotice) "
+                        "iAPS v\(state.versionNumber) (\(state.buildNumber))\nBranch: \(state.branch) \(state.copyrightNotice) "
                     ).textCase(nil)
                 ) {
                     Toggle("Closed loop", isOn: $state.closedLoop)
@@ -43,6 +43,7 @@ extension Settings {
                     Text("Carb Ratios").navigationLink(to: .crEditor, from: self)
                     Text("Target Glucose").navigationLink(to: .targetsEditor, from: self)
                     Text("Autotune").navigationLink(to: .autotuneConfig, from: self)
+                    Text("Bolus Calculator").navigationLink(to: .bolusCalculatorConfig, from: self)
                 }
 
                 Section(header: Text("Developer")) {
@@ -51,7 +52,7 @@ extension Settings {
                         Group {
                             HStack {
                                 Text("NS Upload Profile and Settings")
-                                Button("Upload") { state.uploadProfileAndSettings() }
+                                Button("Upload") { state.uploadProfileAndSettings(true) }
                                     .frame(maxWidth: .infinity, alignment: .trailing)
                                     .buttonStyle(.borderedProminent)
                             }
@@ -126,7 +127,8 @@ extension Settings {
             .onAppear(perform: configureView)
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))
-            .navigationBarTitleDisplayMode(.automatic)
+            .navigationBarTitleDisplayMode(.inline)
+            .onDisappear(perform: { state.uploadProfileAndSettings(false) })
         }
     }
 }

+ 1 - 1
FreeAPS/Sources/Modules/Stat/View/StatRootView.swift

@@ -148,7 +148,7 @@ extension Stat {
             }
             .onAppear(perform: configureView)
             .navigationBarTitle("Statistics")
-            .navigationBarTitleDisplayMode(.automatic)
+            .navigationBarTitleDisplayMode(.inline)
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
         }
     }

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

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

+ 18 - 26
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -194,7 +194,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             let sampleIDs = samples.compactMap(\.syncIdentifier)
             let sampleDates = samples.map(\.startDate)
             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
                 .map {
                     HKQuantitySample(
@@ -227,23 +227,22 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
               events.isNotEmpty
         else { return }
 
-        func delete(syncIds: [String]?) {
-            syncIds?.forEach { syncID in
+        func save(bolusToModify: [InsulinBolus], bolus: [InsulinBolus], basal: [InsulinBasal]) {
+            // first step : delete the HK value
+            // second step : recreate with the new value !
+            bolusToModify.forEach { syncID in
                 let predicate = HKQuery.predicateForObjects(
                     withMetadataKey: HKMetadataKeySyncIdentifier,
                     operatorType: .equalTo,
                     value: syncID
                 )
-
                 self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
                     guard let error = error else { return }
                     warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
                 }
             }
-        }
-
-        func save(bolus: [InsulinBolus], basal: [InsulinBasal]) {
-            let bolusSamples = bolus
+            let bolusTotal = bolus + bolusToModify
+            let bolusSamples = bolusTotal
                 .map {
                     HKQuantitySample(
                         type: sampleType,
@@ -280,30 +279,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             healthKitStore.save(bolusSamples + basalSamples) { _, _ in }
         }
 
-        // delete existing event in HK where the amount is not the last value in the pumphistory
         loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
             .receive(on: processQueue)
-            .compactMap { samples -> [String] in
+            .compactMap { samples -> ([InsulinBolus], [InsulinBolus], [InsulinBasal]) in
                 let sampleIDs = samples.compactMap(\.syncIdentifier)
-                let bolusToDelete = events
+                let bolusToModify = events
                     .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
-                    .compactMap { event -> String? in
+                    .compactMap { event -> InsulinBolus? in
                         guard let amount = event.amount else { return nil }
                         guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
                         else { return nil }
                         if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
-                            return sampleAmount.syncIdentifier
+                            return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
                         } else { return nil }
                     }
-                return bolusToDelete
-            }
-            .sink(receiveValue: delete)
-            .store(in: &lifetime)
 
-        loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
-            .receive(on: processQueue)
-            .compactMap { samples -> ([InsulinBolus], [InsulinBasal]) in
-                let sampleIDs = samples.compactMap(\.syncIdentifier)
                 let bolus = events
                     .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
                     .compactMap { event -> InsulinBolus? in
@@ -348,7 +338,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
                             endDelivery: nextBasalEvent.timestamp
                         )
                     }
-                return (bolus, basal)
+                return (bolusToModify, bolus, basal)
             }
             .sink(receiveValue: save)
             .store(in: &lifetime)
@@ -407,13 +397,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
 
     /// Try to load samples from Health store
     private func loadSamplesFromHealth(
-        sampleType: HKQuantityType
+        sampleType: HKQuantityType,
+        limit: Int = 100
     ) -> Future<[HKSample], Never> {
         Future { promise in
             let query = HKSampleQuery(
                 sampleType: sampleType,
                 predicate: nil,
-                limit: 1000,
+                limit: limit,
                 sortDescriptors: nil
             ) { _, results, _ in
                 promise(.success((results as? [HKQuantitySample]) ?? []))
@@ -425,7 +416,8 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
     /// Try to load samples from Health store with id and do some work
     private func loadSamplesFromHealth(
         sampleType: HKQuantityType,
-        withIDs ids: [String]
+        withIDs ids: [String],
+        limit: Int = 100
     ) -> Future<[HKSample], Never> {
         Future { promise in
             let predicate = HKQuery.predicateForObjects(
@@ -436,7 +428,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             let query = HKSampleQuery(
                 sampleType: sampleType,
                 predicate: predicate,
-                limit: 1000,
+                limit: limit,
                 sortDescriptors: nil
             ) { _, results, _ in
                 promise(.success((results as? [HKQuantitySample]) ?? []))

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

@@ -141,17 +141,18 @@ extension NightscoutAPI {
             .eraseToAnyPublisher()
     }
 
-    func deleteCarbs(at date: Date) -> AnyPublisher<Void, Swift.Error> {
+    func deleteCarbs(at uniqueID: String) -> AnyPublisher<Void, Swift.Error> {
         var components = URLComponents()
         components.scheme = url.scheme
         components.host = url.host
         components.port = url.port
         components.path = Config.treatmentsPath
         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(
-                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 {
             request.addValue(secret.sha1(), forHTTPHeaderField: "api-secret")
         }
-        request.httpBody = try! JSONCoding.encoder.encode(treatments)
+        request.httpBody = try? JSONCoding.encoder.encode(treatments)
         request.httpMethod = "POST"
 
         return service.run(request)

+ 43 - 70
FreeAPS/Sources/Services/Network/NightscoutManager.swift

@@ -9,7 +9,7 @@ protocol NightscoutManager: GlucoseSource {
     func fetchCarbs() -> AnyPublisher<[CarbsEntry], Never>
     func fetchTempTargets() -> AnyPublisher<[TempTarget], 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 deleteManualGlucose(at: Date)
     func uploadStatus()
@@ -17,7 +17,7 @@ protocol NightscoutManager: GlucoseSource {
     func uploadManualGlucose()
     func uploadStatistics(dailystat: Statistics)
     func uploadPreferences(_ preferences: Preferences)
-    func uploadProfileAndSettings()
+    func uploadProfileAndSettings(_: Bool)
     var cgmURL: URL? { get }
 }
 
@@ -177,62 +177,32 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
             .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
         healthkitManager.deleteCarbs(syncID: syncID, isFPU: isFPU, fpuID: fpuID)
 
         guard let nightscout = nightscoutAPI, isUploadEnabled else {
-            carbsStorage.deleteCarbs(at: date)
+            carbsStorage.deleteCarbs(at: uniqueID)
             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) {
@@ -445,7 +415,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         }
     }
 
-    func uploadProfileAndSettings() {
+    func uploadProfileAndSettings(_ force: Bool) {
         // These should be modified anyways and not the defaults
         guard let sensitivities = storage.retrieve(OpenAPS.Settings.insulinSensitivities, as: InsulinSensitivities.self),
               let basalProfile = storage.retrieve(OpenAPS.Settings.basalProfile, as: [BasalProfileEntry].self),
@@ -454,7 +424,7 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
               let preferences = storage.retrieve(OpenAPS.Settings.preferences, as: Preferences.self),
               let settings = storage.retrieve(OpenAPS.FreeAPS.settings, as: FreeAPSSettings.self)
         else {
-            NSLog("NightscoutManager uploadProfile Not all settings found to build profile!")
+            debug(.nightscout, "NightscoutManager uploadProfile Not all settings found to build profile!")
             return
         }
 
@@ -548,37 +518,37 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
 
         // UPLOAD PREFERNCES WHEN CHANGED
         if let uploadedPreferences = storage.retrieve(OpenAPS.Nightscout.uploadedPreferences, as: Preferences.self),
-           uploadedPreferences.rawJSON.sorted() == preferences.rawJSON.sorted()
+           uploadedPreferences.rawJSON.sorted() == preferences.rawJSON.sorted(), !force
         {
             NSLog("NightscoutManager Preferences, preferences unchanged")
         } else { uploadPreferences(preferences) }
 
         // UPLOAD FreeAPS Settings WHEN CHANGED
         if let uploadedSettings = storage.retrieve(OpenAPS.Nightscout.uploadedSettings, as: FreeAPSSettings.self),
-           uploadedSettings.rawJSON.sorted() == settings.rawJSON.sorted()
+           uploadedSettings.rawJSON.sorted() == settings.rawJSON.sorted(), !force
         {
             NSLog("NightscoutManager Settings, settings unchanged")
         } else { uploadSettings(settings) }
 
+        // UPLOAD Profiles WHEN CHANGED
         if let uploadedProfile = storage.retrieve(OpenAPS.Nightscout.uploadedProfile, as: NightscoutProfileStore.self),
-           (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted()
+           (uploadedProfile.store["default"]?.rawJSON ?? "").sorted() == ps.rawJSON.sorted(), !force
         {
             NSLog("NightscoutManager uploadProfile, no profile change")
-            return
-        }
-
-        processQueue.async {
-            nightscout.uploadProfile(p)
-                .sink { completion in
-                    switch completion {
-                    case .finished:
-                        self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
-                        debug(.nightscout, "Profile uploaded")
-                    case let .failure(error):
-                        debug(.nightscout, error.localizedDescription)
-                    }
-                } receiveValue: {}
-                .store(in: &self.lifetime)
+        } else {
+            processQueue.async {
+                nightscout.uploadProfile(p)
+                    .sink { completion in
+                        switch completion {
+                        case .finished:
+                            self.storage.save(p, as: OpenAPS.Nightscout.uploadedProfile)
+                            debug(.nightscout, "Profile uploaded")
+                        case let .failure(error):
+                            debug(.nightscout, error.localizedDescription)
+                        }
+                    } receiveValue: {}
+                    .store(in: &self.lifetime)
+            }
         }
     }
 
@@ -610,9 +580,12 @@ final class BaseNightscoutManager: NightscoutManager, Injectable {
         guard !glucose.isEmpty, let nightscout = nightscoutAPI, isUploadEnabled, isUploadGlucoseEnabled else {
             return
         }
+        // check if unique code
+        // var uuid = UUID(uuidString: yourString) This will return nil if yourString is not a valid UUID
+        let glucoseWithoutCorrectID = glucose.filter { UUID(uuidString: $0._id) != nil }
 
         processQueue.async {
-            glucose.chunks(ofCount: 100)
+            glucoseWithoutCorrectID.chunks(ofCount: 100)
                 .map { chunk -> AnyPublisher<Void, Error> in
                     nightscout.uploadGlucose(Array(chunk))
                 }

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

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

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

@@ -11,7 +11,7 @@ import Foundation
 
         carbsStorage.storeCarbs(
             [CarbsEntry(
-                id: UUID().uuidString,
+                collectionID: UUID().uuidString,
                 createdAt: dateAdded,
                 carbs: carbs,
                 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.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 {
             DispatchQueue.main.async {
                 textfield.becomeFirstResponder()