Sfoglia il codice sorgente

Merge remote-tracking branch 'upstream/dev' into max_bolus_limit

Mike Plante 2 anni fa
parent
commit
662534e678
38 ha cambiato i file con 234 aggiunte e 98 eliminazioni
  1. 3 3
      FreeAPS/Sources/APS/APSManager.swift
  2. 2 2
      FreeAPS/Sources/APS/Storage/CarbsStorage.swift
  3. 11 2
      FreeAPS/Sources/APS/Storage/PumpHistoryStorage.swift
  4. 1 1
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  5. 1 1
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  6. 2 2
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  7. 2 2
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  8. 1 1
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  9. 1 1
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  10. 1 1
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  11. 1 1
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  12. 1 1
      FreeAPS/Sources/Localizations/Main/hu.lproj/Localizable.strings
  13. 1 1
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  14. 1 1
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  15. 1 1
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  16. 1 1
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  17. 1 1
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  18. 1 1
      FreeAPS/Sources/Localizations/Main/pt-PT.lproj/Localizable.strings
  19. 1 1
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  20. 1 1
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  21. 1 1
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  22. 1 1
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  23. 1 1
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  24. 1 1
      FreeAPS/Sources/Localizations/Main/vi.lproj/Localizable.strings
  25. 1 1
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  26. 0 2
      FreeAPS/Sources/Models/CarbsEntry.swift
  27. 7 2
      FreeAPS/Sources/Models/PumpHistoryEvent.swift
  28. 1 2
      FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift
  29. 2 2
      FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift
  30. 3 43
      FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift
  31. 18 8
      FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift
  32. 6 0
      FreeAPS/Sources/Modules/DataTable/DataTableProvider.swift
  33. 47 1
      FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift
  34. 105 2
      FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift
  35. 1 1
      FreeAPS/Sources/Modules/IconConfig/View/IconSelection.swift
  36. 3 3
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  37. 0 1
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  38. 1 0
      fastlane/Fastfile

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

@@ -1256,7 +1256,7 @@ private extension PumpManager {
                     debug(.apsManager, "Temp basal failed: \(unitsPerHour) for: \(duration)")
                     promise(.failure(error))
                 } else {
-                    debug(.apsManager, "Temp basal succeded: \(unitsPerHour) for: \(duration)")
+                    debug(.apsManager, "Temp basal succeeded: \(unitsPerHour) for: \(duration)")
                     promise(.success(nil))
                 }
             }
@@ -1275,7 +1275,7 @@ private extension PumpManager {
                     debug(.apsManager, "Bolus failed: \(units)")
                     promise(.failure(error))
                 } else {
-                    debug(.apsManager, "Bolus succeded: \(units)")
+                    debug(.apsManager, "Bolus succeeded: \(units)")
                     promise(.success(nil))
                 }
             }
@@ -1289,7 +1289,7 @@ private extension PumpManager {
             self.cancelBolus { result in
                 switch result {
                 case let .success(dose):
-                    debug(.apsManager, "Cancel Bolus succeded")
+                    debug(.apsManager, "Cancel Bolus succeeded")
                     promise(.success(dose))
                 case let .failure(error):
                     debug(.apsManager, "Cancel Bolus failed")

+ 2 - 2
FreeAPS/Sources/APS/Storage/CarbsStorage.swift

@@ -81,7 +81,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                     } else { useDate = useDate.addingTimeInterval(interval.minutes.timeInterval) }
 
                     let eachCarbEntry = CarbsEntry(
-                        id: UUID().uuidString, createdAt: entries.last?.createdAt ?? Date(), actualDate: useDate,
+                        id: UUID().uuidString, createdAt: useDate,
                         carbs: equivalent, fat: 0, protein: 0, note: nil,
                         enteredBy: CarbsEntry.manual, isFPU: true,
                         fpuID: fpuID
@@ -92,7 +92,7 @@ final class BaseCarbsStorage: CarbsStorage, Injectable {
                 // Save the array
                 if carbEquivalents > 0 {
                     self.storage.transaction { storage in
-                        storage.append(futureCarbArray, to: file, uniqBy: \.createdAt)
+                        storage.append(futureCarbArray, to: file, uniqBy: \.id)
                         uniqEvents = storage.retrieve(file, as: [CarbsEntry].self)?
                             .filter { $0.createdAt.addingTimeInterval(1.days.timeInterval) > Date() }
                             .sorted { $0.createdAt > $1.createdAt } ?? []

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

@@ -44,7 +44,8 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                         durationMin: nil,
                         rate: nil,
                         temp: nil,
-                        carbInput: nil
+                        carbInput: nil,
+                        isExternalInsulin: dose.manuallyEntered
                     )]
                 case .tempBasal:
                     guard let dose = event.dose else { return [] }
@@ -209,6 +210,13 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         }
     }
 
+    func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
+        if event.isExternalInsulin ?? false {
+            return .externalInsulin
+        }
+        return event.type
+    }
+
     func nightscoutTretmentsNotUploaded() -> [NigtscoutTreatment] {
         let events = recent()
         guard !events.isEmpty else { return [] }
@@ -249,13 +257,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: .bolus,
+                    eventType: eventType,
                     createdAt: event.timestamp,
                     enteredBy: NigtscoutTreatment.local,
                     bolus: event,

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Appikoner";
 
 /* */
-"iAPS Icon" = "iAPS Ikon";
+"Open-iAPS Icon" = "Open-iAPS Ikon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1222,7 +1222,7 @@ Enact a temp Basal or a temp target */
 /* An Automatic delivered bolus (SMB) */
 "SMB" = "SMB";
 
-/* A manually entered dose of external insulin */
+/* A manually entered dose of non-pump insulin */
 "External Insulin" = "Externes Insulin";
 
 /* Status highlight when manual temp basal is running. */
@@ -1374,7 +1374,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App-Symbole";
 
 /* */
-"iAPS Icon" = "iAPS Symbol";
+"Open-iAPS Icon" = "Open-iAPS Symbol";
 
 /* Service Section */
 "Statistics and Home View" = "Statistiken und Home-Ansicht";

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

@@ -1220,7 +1220,7 @@ Enact a temp Basal or a temp target */
 /* An Automatic delivered bolus (SMB) */
 "SMB" = "SMB";
 
-/* A manually entered dose of external insulin */
+/* A manually entered dose of non-pump insulin */
 "External Insulin" = "External Insulin";
 
 /* Status highlight when manual temp basal is running. */
@@ -1372,7 +1372,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1370,7 +1370,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Icône de l’app";
 
 /* */
-"iAPS Icon" = "Icône iAPS";
+"Open-iAPS Icon" = "Icône Open-iAPS";
 
 /* Service Section */
 "Statistics and Home View" = "Statistiques et Vue Accueil";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Icone App";
 
 /* */
-"iAPS Icon" = "Icona di iAPS";
+"Open-iAPS Icon" = "Icona di Open-iAPS";
 
 /* Service Section */
 "Statistics and Home View" = "Statistiche e Vista Iniziale";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App-ikon";
 
 /* */
-"iAPS Icon" = "iAPS ikon";
+"Open-iAPS Icon" = "Open-iAPS ikon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistikk og startskjerm";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App iconen";
 
 /* */
-"iAPS Icon" = "iAPS icoon";
+"Open-iAPS Icon" = "Open-iAPS icoon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistieken en Home View";

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

@@ -1373,7 +1373,7 @@ Połączono z Nightscout!";
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1374,7 +1374,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Иконки приложения";
 
 /* */
-"iAPS Icon" = "iAPS иконка";
+"Open-iAPS Icon" = "Open-iAPS иконка";
 
 /* Service Section */
 "Statistics and Home View" = "Статистика и экран";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Ikony aplikácie";
 
 /* */
-"iAPS Icon" = "ikona iAPS";
+"Open-iAPS Icon" = "ikona Open-iAPS";
 
 /* Service Section */
 "Statistics and Home View" = "Štatistiky a domovské zobrazenie";

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

@@ -1374,7 +1374,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Ikoner";
 
 /* */
-"iAPS Icon" = "iAPS - ikon";
+"Open-iAPS Icon" = "Open-iAPS - ikon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistik och Diagram";

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

@@ -1375,7 +1375,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Іконки Додатку";
 
 /* */
-"iAPS Icon" = "iAPS значок";
+"Open-iAPS Icon" = "Open-iAPS значок";
 
 /* Service Section */
 "Statistics and Home View" = "Статистика та Домашня сторінка";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "Biểu tượng ứng dụng";
 
 /* */
-"iAPS Icon" = "biểu tượng iAPS";
+"Open-iAPS Icon" = "biểu tượng Open-iAPS";
 
 /* Service Section */
 "Statistics and Home View" = "Thống kê và xem";

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

@@ -1371,7 +1371,7 @@ Enact a temp Basal or a temp target */
 "App Icons" = "App Icons";
 
 /* */
-"iAPS Icon" = "iAPS Icon";
+"Open-iAPS Icon" = "Open-iAPS Icon";
 
 /* Service Section */
 "Statistics and Home View" = "Statistics and Home View";

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

@@ -3,7 +3,6 @@ import Foundation
 struct CarbsEntry: JSON, Equatable, Hashable {
     let id: String?
     let createdAt: Date
-    let actualDate: Date?
     let carbs: Decimal
     let fat: Decimal?
     let protein: Decimal?
@@ -28,7 +27,6 @@ extension CarbsEntry {
     private enum CodingKeys: String, CodingKey {
         case id = "_id"
         case createdAt = "created_at"
-        case actualDate
         case carbs
         case fat
         case protein

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

@@ -11,6 +11,7 @@ struct PumpHistoryEvent: JSON, Equatable {
     let temp: TempType?
     let carbInput: Int?
     let note: String?
+    let isExternalInsulin: Bool?
 
     init(
         id: String,
@@ -22,7 +23,8 @@ struct PumpHistoryEvent: JSON, Equatable {
         rate: Decimal? = nil,
         temp: TempType? = nil,
         carbInput: Int? = nil,
-        note: String? = nil
+        note: String? = nil,
+        isExternalInsulin: Bool? = nil
     ) {
         self.id = id
         self.type = type
@@ -34,12 +36,14 @@ struct PumpHistoryEvent: JSON, Equatable {
         self.temp = temp
         self.carbInput = carbInput
         self.note = note
+        self.isExternalInsulin = isExternalInsulin
     }
 }
 
 enum EventType: String, JSON {
     case bolus = "Bolus"
-    case mealBulus = "Meal Bolus"
+    case externalInsulin = "External Insulin"
+    case mealBolus = "Meal Bolus"
     case correctionBolus = "Correction Bolus"
     case snackBolus = "Snack Bolus"
     case bolusWizard = "BolusWizard"
@@ -80,5 +84,6 @@ extension PumpHistoryEvent {
         case temp
         case carbInput = "carb_input"
         case note
+        case isExternalInsulin
     }
 }

+ 1 - 2
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -37,8 +37,7 @@ extension AddCarbs {
             carbsStorage.storeCarbs(
                 [CarbsEntry(
                     id: UUID().uuidString,
-                    createdAt: Date.now,
-                    actualDate: date,
+                    createdAt: date,
                     carbs: carbs,
                     fat: fat,
                     protein: protein,

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

@@ -73,7 +73,6 @@ extension Bolus {
                 showModal(for: nil)
                 return
             }
-            amount = min(amount, maxBolus * 3) // Allow for 3 * Max Bolus for non-pump insulin
 
             pumpHistoryStorage.storeEvents(
                 [
@@ -86,7 +85,8 @@ extension Bolus {
                         durationMin: nil,
                         rate: nil,
                         temp: nil,
-                        carbInput: nil
+                        carbInput: nil,
+                        isExternalInsulin: true
                     )
                 ]
             )

+ 3 - 43
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -83,51 +83,11 @@ extension Bolus {
                                 state.amount <= 0 || state.amount > state.maxBolus
                             )
                     }
-                    Section {
-                        if waitForSuggestion {
+                    if waitForSuggestion {
+                        Section {
                             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 addOverMax = NSLocalizedString(
-                            "\nAmount is more than your Max Bolus setting! \nAre you sure you want to add ",
-                            comment: "Alert"
-                        )
-                        let addUnderMax = NSLocalizedString("Add", comment: "Add insulin without bolusing alert")
-                        let insulinUnit = NSLocalizedString(" U", comment: "Insulin unit")
-                        let withoutBolusing = NSLocalizedString(
-                            " without bolusing",
-                            comment: "Add insulin without bolusing alert"
-                        )
-                        let insulinAmount = formatter.string(from: state.amount as NSNumber)!
-
-                        let overMaxBolusString = addOverMax + insulinAmount + insulinUnit + withoutBolusing + "?"
-                        let underMaxBolusString = addUnderMax + " " + insulinAmount + insulinUnit + withoutBolusing
-
-                        // Actual alert
-                        return Alert(
-                            title: Text(
-                                isOverMax ? "Warning" : "Are you sure?"
-                            ),
-                            message:
-                            Text(
-                                isOverMax ? overMaxBolusString : underMaxBolusString
-                            ),
-                            primaryButton: .destructive(
-                                Text("Add"),
-                                action: {
-                                    state.addWithoutBolus()
-                                    isAddInsulinAlertPresented = false
-                                }
-                            ),
-                            secondaryButton: .cancel()
-                        )
+                        }.frame(maxWidth: .infinity, alignment: .center)
                     }
                 }
             }

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

@@ -66,8 +66,9 @@ enum DataTable {
         let isFPU: Bool?
         let fpuID: String?
         let note: String?
+        let isExternal: Bool?
 
-        private var numberFormater: NumberFormatter {
+        private var numberFormatter: NumberFormatter {
             let formatter = NumberFormatter()
             formatter.numberStyle = .decimal
             formatter.maximumFractionDigits = 2
@@ -92,7 +93,8 @@ enum DataTable {
             idPumpEvent: String? = nil,
             isFPU: Bool? = false,
             fpuID: String? = nil,
-            note: String? = nil
+            note: String? = nil,
+            isExternal: Bool? = nil
         ) {
             self.units = units
             self.type = type
@@ -105,6 +107,7 @@ enum DataTable {
             self.isFPU = isFPU
             self.fpuID = fpuID
             self.note = note
+            self.isExternal = isExternal
         }
 
         static func == (lhs: Treatment, rhs: Treatment) -> Bool {
@@ -126,14 +129,21 @@ enum DataTable {
 
             switch type {
             case .carbs:
-                return numberFormater.string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carbs")
+                return numberFormatter.string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carbs")
             case .fpus:
-                return numberFormater
+                return numberFormatter
                     .string(from: amount as NSNumber)! + NSLocalizedString(" g", comment: "gram of carb equilvalents")
             case .bolus:
-                return numberFormater.string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
+                var bolusText = ""
+
+                if isExternal ?? false {
+                    bolusText += " " + NSLocalizedString("External", comment: "External Insulin")
+                }
+
+                return numberFormatter
+                    .string(from: amount as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit") + bolusText
             case .tempBasal:
-                return numberFormater
+                return numberFormatter
                     .string(from: amount as NSNumber)! + NSLocalizedString(" U/hr", comment: "Unit insulin per hour")
             case .tempTarget:
                 var converted = amount
@@ -142,7 +152,7 @@ enum DataTable {
                 }
 
                 guard var secondAmount = secondAmount else {
-                    return numberFormater.string(from: converted as NSNumber)! + " \(units.rawValue)"
+                    return numberFormatter.string(from: converted as NSNumber)! + " \(units.rawValue)"
                 }
                 if units == .mmolL {
                     secondAmount = secondAmount.asMmolL
@@ -177,7 +187,7 @@ enum DataTable {
             guard let duration = duration, duration > 0 else {
                 return nil
             }
-            return numberFormater.string(from: duration as NSNumber)! + " min"
+            return numberFormatter.string(from: duration as NSNumber)! + " min"
         }
     }
 

+ 6 - 0
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()
         }

+ 47 - 1
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!
 
         let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
 
@@ -13,11 +14,15 @@ extension DataTable {
         @Published var treatments: [Treatment] = []
         @Published var glucose: [Glucose] = []
         @Published var manualGlcuose: 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)
@@ -66,7 +71,14 @@ extension DataTable {
                 let boluses = self.provider.pumpHistory()
                     .filter { $0.type == .bolus }
                     .map {
-                        Treatment(units: units, type: .bolus, date: $0.timestamp, amount: $0.amount, idPumpEvent: $0.id)
+                        Treatment(
+                            units: units,
+                            type: .bolus,
+                            date: $0.timestamp,
+                            amount: $0.amount,
+                            idPumpEvent: $0.id,
+                            isExternal: $0.isExternalInsulin
+                        )
                     }
 
                 let tempBasals = self.provider.pumpHistory()
@@ -181,6 +193,40 @@ extension DataTable {
             provider.glucoseStorage.storeGlucose([saveToJSON])
             debug(.default, "Manual Glucose saved to glucose.json")
         }
+
+        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,
+                                isExternalInsulin: true
+                            )
+                        ]
+                    )
+                    debug(.default, "External insulin saved to pumphistory.json")
+
+                    // Reset amount to 0 for next entry
+                    externalInsulinAmount = 0
+                }
+                .store(in: &lifetime)
+        }
     }
 }
 

+ 105 - 2
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -12,9 +12,18 @@ extension DataTable {
         @State private var isRemoveInsulinAlertPresented = false
         @State private var removeInsulinAlert: Alert?
         @State private var newGlucose = false
+        @State private var showExternalInsulin = false
+        @State private var isAmountUnconfirmed = 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
@@ -57,6 +66,14 @@ extension DataTable {
                 leading: Button("Close", action: state.hideModal),
                 trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
             )
+            .sheet(isPresented: $showExternalInsulin, onDismiss: {
+                if isAmountUnconfirmed {
+                    state.externalInsulinAmount = 0
+                    state.externalInsulinDate = Date()
+                }
+            }) {
+                addExternalInsulinView
+            }
             .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) {
                 VStack(spacing: 20) {
                     HStack {
@@ -90,8 +107,30 @@ extension DataTable {
 
         private var treatmentsList: some View {
             List {
-                ForEach(state.treatments) { item in
-                    treatmentView(item)
+                HStack {
+                    Spacer()
+                    Button(action: { showExternalInsulin = true
+                        state.externalInsulinDate = Date() }, label: {
+                        HStack {
+                            Text("Add")
+                                .foregroundColor(Color.secondary)
+                                .font(.caption)
+
+                            Image(systemName: "syringe")
+                                .foregroundColor(Color.accentColor)
+                        }.frame(maxWidth: .infinity, alignment: .trailing)
+
+                    }).buttonStyle(.borderless)
+                }
+
+                if !state.treatments.isEmpty {
+                    ForEach(state.treatments) { item in
+                        treatmentView(item)
+                    }
+                } else {
+                    HStack {
+                        Text("No data.")
+                    }
                 }
             }
         }
@@ -191,6 +230,70 @@ extension DataTable {
             }
         }
 
+        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) &&
+                            (state.externalInsulinAmount <= state.maxBolus * 3)
+
+                        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) -> some View {
             VStack(alignment: .leading, spacing: 4) {
                 HStack {

+ 1 - 1
FreeAPS/Sources/Modules/IconConfig/View/IconSelection.swift

@@ -9,7 +9,7 @@ struct IconSelection: View {
 
         VStack {
             HStack {
-                Text("iAPS Icon")
+                Text("Open-iAPS Icon")
                     .font(.title)
                 IconImage(icon: model.appIcon)
                     .frame(maxHeight: 114)

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

@@ -204,13 +204,13 @@ final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, P
             let sampleDates = samples.map(\.startDate)
             let samplesToSave = carbsWithId
                 .filter { !sampleIDs.contains($0.id ?? "") } // id existing in AH
-                .filter { !sampleDates.contains($0.actualDate ?? $0.createdAt) } // not id but exactly the same datetime
+                .filter { !sampleDates.contains($0.createdAt) } // not id but exactly the same datetime
                 .map {
                     HKQuantitySample(
                         type: sampleType,
                         quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
-                        start: $0.actualDate ?? $0.createdAt,
-                        end: $0.actualDate ?? $0.createdAt,
+                        start: $0.createdAt,
+                        end: $0.createdAt,
                         metadata: [
                             HKMetadataKeySyncIdentifier: $0.id ?? "_id",
                             HKMetadataKeySyncVersion: 1,

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

@@ -274,7 +274,6 @@ extension BaseWatchManager: WCSessionDelegate {
                 [CarbsEntry(
                     id: UUID().uuidString,
                     createdAt: Date.now,
-                    actualDate: Date.now,
                     carbs: Decimal(carbs),
                     fat: Decimal(fat),
                     protein: Decimal(protein), note: nil,

+ 1 - 0
fastlane/Fastfile

@@ -153,6 +153,7 @@ platform :ios do
       skip_submission: false,
       ipa: "iAPS.ipa",
       skip_waiting_for_build_processing: true,
+      changelog: git_branch+" "+last_git_commit[:abbreviated_commit_hash],
     )
   end