Explorar el Código

Release/0.2.5 (#163)

* Bump dev version to 0.2.5

* Loop routine refactored

* reportEnacted only in closed loop

* Crowdin (#161)

* Crowdin

* Crowdin updates (#404)

* Upload status in closed loop only

* Decimal precision for sensitive values

* Pause animated background when app is in background

* Home view refactored

* DecimalTextField fixes

Co-authored-by: Jon B Mårtensson <53905247+Jon-b-m@users.noreply.github.com>
Ivan hace 4 años
padre
commit
d6276df257
Se han modificado 35 ficheros con 722 adiciones y 512 borrados
  1. 1 1
      Config.xcconfig
  2. 2 2
      Dependencies/LoopKit/LoopKit/tr.lproj/Localizable.strings
  3. 1 1
      Dependencies/LoopKit/LoopKitUI/tr.lproj/Localizable.strings
  4. 23 23
      Dependencies/rileylink_ios/OmniKitUI/tr.lproj/OmnipodPumpManager.strings
  5. 7 7
      FreeAPS/Resources/fr.lproj/InfoPlist.strings
  6. 107 71
      FreeAPS/Sources/APS/APSManager.swift
  7. 25 0
      FreeAPS/Sources/AnimatedBackground/SnowScene.swift
  8. 10 0
      FreeAPS/Sources/Helpers/String+Extensions.swift
  9. 3 0
      FreeAPS/Sources/Localizations/Main/ar.lproj/Localizable.strings
  10. 3 0
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  11. 3 0
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  12. 3 0
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  13. 3 0
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  14. 3 0
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  15. 179 173
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  16. 3 0
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  17. 3 0
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  18. 8 5
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  19. 142 139
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  20. 3 0
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  21. 3 0
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  22. 1 0
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  23. 3 0
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  24. 3 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  25. 16 13
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  26. 3 0
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  27. 3 0
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  28. 17 0
      FreeAPS/Sources/Models/BasalProfileEntry.swift
  29. 17 0
      FreeAPS/Sources/Models/CarbRatios.swift
  30. 17 0
      FreeAPS/Sources/Models/InsulinSensitivities.swift
  31. 2 1
      FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift
  32. 1 1
      FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift
  33. 86 72
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  34. 1 1
      FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift
  35. 17 2
      FreeAPS/Sources/Views/DecimalTextField.swift

+ 1 - 1
Config.xcconfig

@@ -1,5 +1,5 @@
 APP_DISPLAY_NAME = FreeAPS X
-BUILD_VERSION = 0.2.4
+BUILD_VERSION = 0.2.5
 DEVELOPER_TEAM = ##TEAM_ID##
 BUNDLE_IDENTIFIER = ru.artpancreas.$(DEVELOPMENT_TEAM).FreeAPS
 APP_GROUP_ID = group.com.$(DEVELOPMENT_TEAM).loopkit.LoopGroup

+ 2 - 2
Dependencies/LoopKit/LoopKit/tr.lproj/Localizable.strings

@@ -14,7 +14,7 @@
 "com.loudnate.CarbKit.deleteCarbEntryUnownedErrorDescription" = "Yetkilendirme Reddedildi";
 
 /* The error recovery suggestion when attempting to delete a sample not shared by the current app */
-"com.loudnate.carbKit.sharingDeniedErrorRecoverySuggestion" = "Bu örnek Sağlık uygulamasından silinebilir";
+"com.loudnate.carbKit.sharingDeniedErrorRecoverySuggestion" = "Bu örnek Health uygulamasından silinebilir";
 
 /* Generic pump error description */
 "Communication Failure" = "İletişim Hatası";
@@ -68,7 +68,7 @@
 "OK" = "Tamam";
 
 /* The error recovery suggestion when Health sharing was denied */
-"Please re-enable sharing in Health" = "Lütfen Sağlık Uygulaması paylaşımını yeniden etkinleştirin";
+"Please re-enable sharing in Health" = "Lütfen Health paylaşımı yeniden etkinleştirin";
 
 /* Glucose trend up */
 "Rising" = "Yükselmekte";

+ 1 - 1
Dependencies/LoopKit/LoopKitUI/tr.lproj/Localizable.strings

@@ -45,7 +45,7 @@
 "Basal, bolus, and correction insulin dose amounts are unaffected." = "Bazal, bolus ve düzeltme insülin doz miktarları etkilenmez.";
 
 /* The title of the cancel action in an action sheet */
-"Cancel" = "İptal Et";
+"Cancel" = "Vazgeç";
 
 /* The text for the override cancellation button */
 "Cancel Override" = "Geçersiz kılmayı İptal et";

+ 23 - 23
Dependencies/rileylink_ios/OmniKitUI/tr.lproj/OmnipodPumpManager.strings

@@ -1,68 +1,68 @@
 /* Class = "UITableViewSection"; headerTitle = "Remove POD"; ObjectID = "1LF-te-Bdd"; */
-"1LF-te-Bdd.headerTitle" = "Remove POD";
+"1LF-te-Bdd.headerTitle" = "POD'u kaldır";
 
 /* Class = "UINavigationItem"; title = "RileyLink Setup"; ObjectID = "3HH-eJ-lRh"; */
-"3HH-eJ-lRh.title" = "RileyLink Setup";
+"3HH-eJ-lRh.title" = "RileyLink Kurulumu";
 
 /* Class = "UITableViewController"; title = "Pod Settings"; ObjectID = "6vo-Ov-UpE"; */
-"6vo-Ov-UpE.title" = "Pod Settings";
+"6vo-Ov-UpE.title" = "Pod Ayarları";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "91O-Un-vKc"; */
-"91O-Un-vKc.title" = "Pump Setup";
+"91O-Un-vKc.title" = "Pompa Kurulumu";
 
 /* Class = "UITableViewSection"; footerTitle = "NOTE: Do not remove the pod's needle cap at this time."; ObjectID = "EUt-xk-Rmp"; */
-"EUt-xk-Rmp.footerTitle" = "NOTE: Do not remove the pod's needle cap at this time.";
+"EUt-xk-Rmp.footerTitle" = "NOT: Şu anda pod'un plastik iğne kapağını çıkarmayın.";
 
 /* Class = "UITableViewSection"; headerTitle = "Prepare Pod"; ObjectID = "EUt-xk-Rmp"; */
-"EUt-xk-Rmp.headerTitle" = "Prepare Pod";
+"EUt-xk-Rmp.headerTitle" = "Pod'u Hazırla";
 
 /* Class = "UILabel"; text = "Loop will remind you to change your pod before it expires. You can change this to a time convenient for you."; ObjectID = "Eng-IY-fQ7"; */
-"Eng-IY-fQ7.text" = "Loop will remind you to change your pod before it expires. You can change this to a time convenient for you.";
+"Eng-IY-fQ7.text" = "Döngü, süresi dolmadan önce pod'u değiştirmenizi hatırlatacaktır. Bunun için değişimi size uygun olan bir zamanda yapabilirsiniz.";
 
 /* Class = "UILabel"; text = "Please deactivate the pod. When deactivation is complete, remove pod from body."; ObjectID = "GK7-jb-tyY"; */
-"GK7-jb-tyY.text" = "Please deactivate the pod. When deactivation is complete, remove pod from body.";
+"GK7-jb-tyY.text" = "Lütfen pod'u devre dışı bırakın. Devre dışı bırakma tamamlandığında, pod'u vücudunuzdan çıkarın.";
 
 /* Class = "UINavigationItem"; title = "Insert Cannula"; ObjectID = "HwT-30-f0y"; */
-"HwT-30-f0y.title" = "Insert Cannula";
+"HwT-30-f0y.title" = "Kanül Yerleştir";
 
 /* Class = "UILabel"; text = "Prepare site. Remove the pod's needle cap and adhesive backing. If pod is OK, apply to site."; ObjectID = "Iuv-5M-bDH"; */
-"Iuv-5M-bDH.text" = "Prepare site. Remove the pod's needle cap and adhesive backing. If pod is OK, apply to site.";
+"Iuv-5M-bDH.text" = "İnfüzyon bölgesini hazırlayın. Pod'un plastik iğne kapağını ve yapışkan desteğini çıkarın. Herşey yolunda ise pod'u infüzyon bölgesine yapıştırın.";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "aNg-mm-Uuy"; */
-"aNg-mm-Uuy.title" = "Pump Setup";
+"aNg-mm-Uuy.title" = "Pompa Kurulumu";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "ack-ra-XH6"; */
-"ack-ra-XH6.title" = "Pump Setup";
+"ack-ra-XH6.title" = "Pompa Kurulumu";
 
 /* Class = "UILabel"; text = "Your Pod is ready for use."; ObjectID = "bJ5-iH-fnF"; */
-"bJ5-iH-fnF.text" = "Your Pod is ready for use.";
+"bJ5-iH-fnF.text" = "Pod'unuz kullanıma hazır.";
 
 /* Class = "UILabel"; text = "Reminder"; ObjectID = "ePA-6p-q8C"; */
-"ePA-6p-q8C.text" = "Reminder";
+"ePA-6p-q8C.text" = "Hatırlatıcı";
 
 /* Class = "UINavigationItem"; title = "Pod Pairing"; ObjectID = "jVO-Ut-MhL"; */
-"jVO-Ut-MhL.title" = "Pod Pairing";
+"jVO-Ut-MhL.title" = "Pod Eşleştiriliyor";
 
 /* Class = "UITableViewController"; title = "Pump Setup"; ObjectID = "k1Y-x4-m0a"; */
-"k1Y-x4-m0a.title" = "Pump Setup";
+"k1Y-x4-m0a.title" = "Pompa Kurulumu";
 
 /* Class = "UILabel"; text = "Review your settings below. They will be programmed into the pod during pairing. You can change these settings at any time in Loopʼs Settings screen."; ObjectID = "kLL-SQ-K0a"; */
-"kLL-SQ-K0a.text" = "Review your settings below. They will be programmed into the pod during pairing. You can change these settings at any time in Loopʼs Settings screen.";
+"kLL-SQ-K0a.text" = "Aşağıdaki ayarlarınızı gözden geçirin. Pod eşleştirme sırasında bu ayarlar programlanacaktır. Bu ayarları istediğiniz zaman Loop'un Ayarlar ekranından değiştirebilirsiniz.";
 
 /* Class = "UINavigationItem"; title = "Setup Complete"; ObjectID = "nDb-R5-e02"; */
-"nDb-R5-e02.title" = "Setup Complete";
+"nDb-R5-e02.title" = "Kurulum tamamlandı";
 
 /* Class = "UITableViewSection"; footerTitle = "NOTE: If cannula sticks out, press cancel."; ObjectID = "rcC-ke-lUP"; */
-"rcC-ke-lUP.footerTitle" = "NOTE: If cannula sticks out, press cancel.";
+"rcC-ke-lUP.footerTitle" = "NOT: Kanül dışarı çıkarsa, iptal düğmesine basın.";
 
 /* Class = "UITableViewSection"; headerTitle = "Apply POD"; ObjectID = "rcC-ke-lUP"; */
-"rcC-ke-lUP.headerTitle" = "Apply POD";
+"rcC-ke-lUP.headerTitle" = "POD'u yerleştirin";
 
 /* Class = "UILabel"; text = "Label"; ObjectID = "vEc-Km-ewe"; */
-"vEc-Km-ewe.text" = "Label";
+"vEc-Km-ewe.text" = "Etiket";
 
 /* Class = "UILabel"; text = "Fill a new pod with insulin. Listen for 2 beeps from the pod during filling. Keep RileyLink adjacent to the pod during pairing."; ObjectID = "vmF-Dc-3DS"; */
-"vmF-Dc-3DS.text" = "Fill a new pod with insulin. Listen for 2 beeps from the pod during filling. Keep the RileyLink about 6 inches from the pod during pairing.";
+"vmF-Dc-3DS.text" = "Yeni bir pod'u insülinle doldurun. Doldurma sırasında pod'tan gelen 2 bip sesini dinleyin. Eşleştirme sırasında RileyLink'i bölmeden yaklaşık 6 inç ( ≈ 15cm) uzakta tutun.";
 
 /* Class = "UINavigationItem"; title = "Replace Pod"; ObjectID = "yy1-xf-HdR"; */
-"yy1-xf-HdR.title" = "Replace Pod";
+"yy1-xf-HdR.title" = "Pod'u değiştir";

+ 7 - 7
FreeAPS/Resources/fr.lproj/InfoPlist.strings

@@ -1,20 +1,20 @@
 /* Privacy - NFC Scan Usage Description */
-"NFCReaderUsageDescription" = "NFC is used to scan Libre sensors.";
+"NFCReaderUsageDescription" = "NFC est utilisé pour scanner les capteurs Libre.";
 
 /* Privacy - Bluetooth Always Usage Description */
-"NSBluetoothAlwaysUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
+"NSBluetoothAlwaysUsageDescription" = "Bluetooth est utilisé pour communiquer avec la pompe à insuline et les dispositifs de surveillance continue du glucose";
 
 /* Privacy - Bluetooth Peripheral Usage Description */
-"NSBluetoothPeripheralUsageDescription" = "Bluetooth is used to communicate with insulin pump and continuous glucose monitor devices";
+"NSBluetoothPeripheralUsageDescription" = "Bluetooth est utilisé pour communiquer avec la pompe à insuline et les dispositifs de surveillance continue du glucose";
 
 /* Privacy - Face ID Usage Description */
-"NSFaceIDUsageDescription" = "For authorized acces to bolus";
+"NSFaceIDUsageDescription" = "Pour les accès autorisés au bolus";
 
 /* Privacy - Calendars Usage Description */
-"NSCalendarsUsageDescription" = "Calendar is used to create a new glucose events.";
+"NSCalendarsUsageDescription" = "Le calendrier est utilisé pour créer un nouvel événement de glycémie.";
 
 /* Privacy - Health Update Usage Description */
-"NSHealthUpdateUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthUpdateUsageDescription" = "L'application Santé est utilisée pour stocker les données de glycémie";
 
 /* Privacy - Health Share Usage Description */
-"NSHealthShareUsageDescription" = "Health App is used to store blood glucose data";
+"NSHealthShareUsageDescription" = "L'application Santé est utilisée pour stocker les données de glycémie";

+ 107 - 71
FreeAPS/Sources/APS/APSManager.swift

@@ -108,9 +108,7 @@ final class BaseAPSManager: APSManager, Injectable {
         lastLoopDateSubject.send(lastLoopDate)
 
         isLooping
-            .sink { value in
-                self.deviceDataManager.loopInProgress = value
-            }
+            .weakAssign(to: \.deviceDataManager.loopInProgress, on: self)
             .store(in: &lifetime)
     }
 
@@ -147,6 +145,7 @@ final class BaseAPSManager: APSManager, Injectable {
         deviceDataManager.heartbeat(date: date)
     }
 
+    // Loop entry point
     private func loop() {
         guard !isLooping.value else {
             warning(.apsManager, "Already looping, skip")
@@ -156,51 +155,71 @@ final class BaseAPSManager: APSManager, Injectable {
         debug(.apsManager, "Starting loop")
         isLooping.send(true)
         determineBasal()
-            .sink { [weak self] ok in
-                guard let self = self else { return }
+            .replaceEmpty(with: false)
+            .flatMap { [weak self] success -> AnyPublisher<Void, Error> in
+                guard let self = self, success else {
+                    return Fail(error: APSError.apsError(message: "Determine basal failed")).eraseToAnyPublisher()
+                }
 
-                if ok {
-                    self.nightscout.uploadStatus()
-                    if self.settings.closedLoop {
-                        self.enactSuggested()
-                    } else {
-                        self.isLooping.send(false)
-                        self.lastLoopDate = Date()
-                    }
+                // Open loop completed
+                guard self.settings.closedLoop else {
+                    return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher()
+                }
+
+                self.nightscout.uploadStatus()
+
+                // Closed loop - enact suggested
+                return self.enactSuggested()
+            }
+            .sink { [weak self] completion in
+                guard let self = self else { return }
+                if case let .failure(error) = completion {
+                    self.loopCompleted(error: error)
                 } else {
-                    self.isLooping.send(false)
+                    self.loopCompleted()
                 }
-            }.store(in: &lifetime)
+            } receiveValue: {}
+            .store(in: &lifetime)
     }
 
-    private func verifyStatus() -> Bool {
+    // Loop exit point
+    private func loopCompleted(error: Error? = nil) {
+        isLooping.send(false)
+
+        if let error = error {
+            warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
+            processError(error)
+        } else {
+            debug(.apsManager, "Loop succeeded")
+            lastLoopDate = Date()
+            lastError.send(nil)
+        }
+
+        if settings.closedLoop {
+            reportEnacted(received: error == nil)
+        }
+    }
+
+    private func verifyStatus() -> Error? {
         guard let pump = pumpManager else {
-            debug(.apsManager, "Pump is not set")
-            processError(APSError.invalidPumpState(message: "Pump not set"))
-            return false
+            return APSError.invalidPumpState(message: "Pump not set")
         }
         let status = pump.status.pumpStatus
 
         guard !status.bolusing else {
-            debug(.apsManager, "Pump is bolusing")
-            processError(APSError.invalidPumpState(message: "Pump is bolusing"))
-            return false
+            return APSError.invalidPumpState(message: "Pump is bolusing")
         }
 
         guard !status.suspended else {
-            debug(.apsManager, "Pump suspended")
-            processError(APSError.invalidPumpState(message: "Pump suspended"))
-            return false
+            return APSError.invalidPumpState(message: "Pump suspended")
         }
 
         let reservoir = storage.retrieve(OpenAPS.Monitor.reservoir, as: Decimal.self) ?? 100
         guard reservoir > 0 else {
-            debug(.apsManager, "Reservoir is empty")
-            processError(APSError.invalidPumpState(message: "Reservoir is empty"))
-            return false
+            return APSError.invalidPumpState(message: "Reservoir is empty")
         }
 
-        return true
+        return nil
     }
 
     private func autosens() -> AnyPublisher<Bool, Never> {
@@ -301,7 +320,17 @@ final class BaseAPSManager: APSManager, Injectable {
     private var bolusReporter: DoseProgressReporter?
 
     func enactBolus(amount: Double, isSMB: Bool) {
-        guard let pump = pumpManager, verifyStatus() else { return }
+        if let error = verifyStatus() {
+            processError(error)
+            processQueue.async {
+                self.broadcaster.notify(BolusFailureObserver.self, on: self.processQueue) {
+                    $0.bolusDidFail()
+                }
+            }
+            return
+        }
+
+        guard let pump = pumpManager else { return }
 
         let roundedAmout = pump.roundToSupportedBolusVolume(units: amount)
 
@@ -348,7 +377,12 @@ final class BaseAPSManager: APSManager, Injectable {
     }
 
     func enactTempBasal(rate: Double, duration: TimeInterval) {
-        guard let pump = pumpManager, verifyStatus() else { return }
+        if let error = verifyStatus() {
+            processError(error)
+            return
+        }
+
+        guard let pump = pumpManager else { return }
         debug(.apsManager, "Enact temp basal \(rate) - \(duration)")
 
         let roundedAmout = pump.roundToSupportedBasalRate(unitsPerHour: rate)
@@ -402,7 +436,8 @@ final class BaseAPSManager: APSManager, Injectable {
 
         switch action {
         case let .bolus(amount):
-            guard verifyStatus() else {
+            if let error = verifyStatus() {
+                processError(error)
                 return
             }
             let roundedAmount = pump.roundToSupportedBolusVolume(units: Double(amount))
@@ -419,7 +454,8 @@ final class BaseAPSManager: APSManager, Injectable {
         case let .pump(pumpAction):
             switch pumpAction {
             case .suspend:
-                guard verifyStatus(), !pump.status.pumpStatus.suspended else {
+                if let error = verifyStatus() {
+                    processError(error)
                     return
                 }
                 pump.suspendDelivery { error in
@@ -450,7 +486,11 @@ final class BaseAPSManager: APSManager, Injectable {
             debug(.apsManager, "Closed loop \(closedLoop) by Announcement")
             announcementsStorage.storeAnnouncements([announcement], enacted: true)
         case let .tempbasal(rate, duration):
-            guard verifyStatus(), !settings.closedLoop else {
+            if let error = verifyStatus() {
+                processError(error)
+                return
+            }
+            guard !settings.closedLoop else {
                 return
             }
             let roundedRate = pump.roundToSupportedBasalRate(unitsPerHour: Double(rate))
@@ -489,30 +529,27 @@ final class BaseAPSManager: APSManager, Injectable {
         }
     }
 
-    private func enactSuggested() {
+    private func enactSuggested() -> AnyPublisher<Void, Error> {
         guard let suggested = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) else {
-            isLooping.send(false)
-            warning(.apsManager, "Suggestion not found")
-            processError(APSError.apsError(message: "Suggestion not found"))
-            return
+            return Fail(error: APSError.apsError(message: "Suggestion not found")).eraseToAnyPublisher()
         }
 
         guard Date().timeIntervalSince(suggested.deliverAt ?? .distantPast) < Config.eхpirationInterval else {
-            isLooping.send(false)
-            warning(.apsManager, "Suggestion expired")
-            processError(APSError.apsError(message: "Suggestion expired"))
-            return
+            return Fail(error: APSError.apsError(message: "Suggestion expired")).eraseToAnyPublisher()
         }
 
         guard let pump = pumpManager else {
-            isLooping.send(false)
-            warning(.apsManager, "Pump not set")
-            processError(APSError.invalidPumpState(message: "Pump not set"))
-            return
+            return Fail(error: APSError.apsError(message: "Pump not set")).eraseToAnyPublisher()
         }
 
         let basalPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
-            guard let rate = suggested.rate, let duration = suggested.duration, self.verifyStatus() else {
+            if let error = self.verifyStatus() {
+                return Fail(error: error).eraseToAnyPublisher()
+            }
+
+            guard let rate = suggested.rate, let duration = suggested.duration else {
+                // It is OK, no temp required
+                debug(.apsManager, "No temp required")
                 return Just(()).setFailureType(to: Error.self)
                     .eraseToAnyPublisher()
             }
@@ -525,7 +562,12 @@ final class BaseAPSManager: APSManager, Injectable {
         }.eraseToAnyPublisher()
 
         let bolusPublisher: AnyPublisher<Void, Error> = Deferred { () -> AnyPublisher<Void, Error> in
-            guard let units = suggested.units, self.verifyStatus() else {
+            if let error = self.verifyStatus() {
+                return Fail(error: error).eraseToAnyPublisher()
+            }
+            guard let units = suggested.units else {
+                // It is OK, no bolus required
+                debug(.apsManager, "No bolus required")
                 return Just(()).setFailureType(to: Error.self)
                     .eraseToAnyPublisher()
             }
@@ -536,26 +578,11 @@ final class BaseAPSManager: APSManager, Injectable {
             .eraseToAnyPublisher()
         }.eraseToAnyPublisher()
 
-        basalPublisher
-            .flatMap { bolusPublisher }
-            .sink { [weak self] completion in
-                if case let .failure(error) = completion {
-                    warning(.apsManager, "Loop failed with error: \(error.localizedDescription)")
-                    self?.reportEnacted(suggestion: suggested, received: false)
-                    self?.processError(APSError.pumpError(error))
-                } else {
-                    self?.reportEnacted(suggestion: suggested, received: true)
-                }
-                self?.isLooping.send(false)
-            } receiveValue: {
-                debug(.apsManager, "Loop succeeded")
-                self.lastError.send(nil)
-                self.lastLoopDate = Date()
-            }.store(in: &lifetime)
+        return basalPublisher.flatMap { bolusPublisher }.eraseToAnyPublisher()
     }
 
-    private func reportEnacted(suggestion: Suggestion, received: Bool) {
-        if suggestion.deliverAt != nil {
+    private func reportEnacted(received: Bool) {
+        if let suggestion = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self), suggestion.deliverAt != nil {
             var enacted = suggestion
             enacted.timestamp = Date()
             enacted.recieved = received
@@ -602,7 +629,9 @@ private extension PumpManager {
                     promise(.failure(error))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 
     func enactBolus(units: Double, automatic: Bool) -> AnyPublisher<DoseEntry, Error> {
@@ -617,7 +646,9 @@ private extension PumpManager {
                     promise(.failure(error))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 
     func cancelBolus() -> AnyPublisher<DoseEntry?, Error> {
@@ -633,6 +664,7 @@ private extension PumpManager {
                 }
             }
         }
+        .mapError { APSError.pumpError($0) }
         .eraseToAnyPublisher()
     }
 
@@ -645,7 +677,9 @@ private extension PumpManager {
                     promise(.success(()))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 
     func resumeDelivery() -> AnyPublisher<Void, Error> {
@@ -657,7 +691,9 @@ private extension PumpManager {
                     promise(.success(()))
                 }
             }
-        }.eraseToAnyPublisher()
+        }
+        .mapError { APSError.pumpError($0) }
+        .eraseToAnyPublisher()
     }
 }
 

+ 25 - 0
FreeAPS/Sources/AnimatedBackground/SnowScene.swift

@@ -1,3 +1,4 @@
+import Foundation
 import SpriteKit
 
 class SnowScene: SKScene {
@@ -9,6 +10,22 @@ class SnowScene: SKScene {
         snowEmitterNode.particleLifetime = 2
         snowEmitterNode.particleLifetimeRange = 6
         addChild(snowEmitterNode)
+        subscribe()
+    }
+
+    private func subscribe() {
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(didEnterBackground),
+            name: UIApplication.didEnterBackgroundNotification,
+            object: nil
+        )
+        Foundation.NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(willEnterForeground),
+            name: UIApplication.willEnterForegroundNotification,
+            object: nil
+        )
     }
 
     override func didChangeSize(_: CGSize) {
@@ -16,4 +33,12 @@ class SnowScene: SKScene {
         snowEmitterNode.particlePosition = CGPoint(x: size.width / 2, y: size.height)
         snowEmitterNode.particlePositionRange = CGVector(dx: size.width, dy: size.height)
     }
+
+    @objc private func didEnterBackground() {
+        isPaused = true
+    }
+
+    @objc private func willEnterForeground() {
+        isPaused = false
+    }
 }

+ 10 - 0
FreeAPS/Sources/Helpers/String+Extensions.swift

@@ -1,3 +1,5 @@
+import Foundation
+
 extension String {
     func capitalizingFirstLetter() -> String {
         prefix(1).capitalized + dropFirst()
@@ -7,3 +9,11 @@ extension String {
         self = capitalizingFirstLetter()
     }
 }
+
+extension LosslessStringConvertible {
+    var string: String { .init(self) }
+}
+
+extension FloatingPoint where Self: LosslessStringConvertible {
+    var decimal: Decimal? { Decimal(string: string) }
+}

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS Haupteinstellungen";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 179 - 173
FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings


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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "Impostazioni principali di OpenAPS";

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

@@ -879,19 +879,22 @@ Enact a temp Basal or a temp target */
 "Bolus failed or inaccurate. Check pump history before repeating." = "Bolus mislyktes eller var upresis. Kontroller bolus-historikken før du gjentar.";
 
 /* */
-"Carbs" = "Carbs";
+"Carbs" = "Karbo";
 
 /* */
-"Temp Basal" = "Temp Basal";
+"Temp Basal" = "Midlertidig basal";
 
 /* */
-"Temp Target" = "Temp Target";
+"Temp Target" = "Midlertidig mål";
 
 /* */
-"Resume" = "Resume";
+"Resume" = "Gjenoppta leveranse";
 
 /* */
-"Suspend" = "Suspend";
+"Suspend" = "Pause leveranse";
+
+/* */
+"Animated Background" = "Animert bakgrunn";
 
 
 /* Headers for settings ----------------------- */

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 142 - 139
FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings


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

@@ -895,6 +895,9 @@ Połączono z Nightscout!";
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -896,6 +896,7 @@ Enact a temp Basal or a temp target */
 /* */
 "Animated Background" = "Анимированный фон";
 
+
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "Основные настройки OpenAPS";
 

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Pausa";
 
+/* */
+"Animated Background" = "Animerad Bakgrund";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS huvudsakliga inställningar";

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

@@ -522,7 +522,7 @@ Enact a temp Basal or a temp target */
 "Pair Sensor & connect" = "Sensör eşleştir ve bağlan";
 
 /* */
-"Phone NFC required!" = "Telefon NFC gerekli!";
+"Phone NFC required!" = "NFC özellikli telefon gerekli!";
 
 /* */
 "Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors" = "Telefonunuz veya uygulamanız, libre2 sensörleriyle eşleştirmek için gerekli olan NFC iletişimleri için etkin değil";
@@ -540,7 +540,7 @@ Enact a temp Basal or a temp target */
 "Delete CGMManager and start anew. Your libreoopweb credentials will be preserved" = "CGMManager'ı silin ve yeniden başlayın. libreoopweb kimlik bilgileriniz korunacaktır";
 
 /* Invalid libre checksum */
-"Invalid libre checksum" = "Geçersiz libre sağlama toplamı";
+"Invalid libre checksum" = "Libre sağlaması geçersiz";
 
 /* Libre sensor was incorrectly read, CRCs were not valid */
 "Libre sensor was incorrectly read, CRCs were not valid" = "Libre sensörü hatalı okundu, CRC'ler geçersizdi";
@@ -570,10 +570,10 @@ Enact a temp Basal or a temp target */
 "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor" = "Bu aralıklı bir sorun olabilir ancak lütfen vericinizin sensörünüze sıkıca sabitlendiğini kontrol edin";
 
 /* New Sensor Detected */
-"New Sensor Detected" = "Yemi Sensör Tespit Edildi";
+"New Sensor Detected" = "Yeni Sensör Algılandı";
 
 /* Please wait up to 30 minutes before glucose readings are available! */
-"Please wait up to 30 minutes before glucose readings are available!" = "Lütfen glikoz ölçümleri mevcut olana kadar 30 dakikaya kadar bekleyin!";
+"Please wait up to 30 minutes before glucose readings are available!" = "Lütfen glikoz ölçümleri mevcut olana kadar 30 dakika bekleyin!";
 
 /* Invalid Glucose sample detected, try again later */
 "Invalid Glucose sample detected, try again later" = "Geçersiz Glikoz örneği tespit edildi, daha sonra tekrar deneyin";
@@ -624,7 +624,7 @@ Enact a temp Basal or a temp target */
 "Last measurement" = "Son ölçüm";
 
 /* */
-"Sensor Footer checksum" = "Sensör Altbilgi sağlama toplamı";
+"Sensor Footer checksum" = "Sensör Altbilgi sağlaması";
 
 /* */
 "Last Blood Sugar prediction" = "Son KŞ'i tahmini";
@@ -642,7 +642,7 @@ Enact a temp Basal or a temp target */
 "Sensor Age Left" = "Kalan Sensör Yaşı";
 
 /* */
-"Sensor Endtime" = "Sensör Bitim zamanı";
+"Sensor Endtime" = "Sensör Bitiş zamanı";
 
 /* */
 "Sensor State" = "Sensör Durumu";
@@ -780,10 +780,10 @@ Enact a temp Basal or a temp target */
 "High Glucose Alarm active" = "Yüksek Glikoz Uyarılarımı aktif";
 
 /* */
-"Low Glucose Alarm active" = "Düşük Glikoz Uyarılarımı aktif";
+"Low Glucose Alarm active" = "Düşük Glikoz Alarmı aktif";
 
 /* */
-"No Glucose Alarm active" = "Glikoz Uyarılarımı aktif değil";
+"No Glucose Alarm active" = "Glikoz Alarmı aktif değil";
 
 /* */
 "snoozing until %@" = "%@'e kadar erteleniyor";
@@ -879,19 +879,22 @@ Enact a temp Basal or a temp target */
 "Bolus failed or inaccurate. Check pump history before repeating." = "Bolus başarısız veya hatalı. Tekrarlamadan önce pompa geçmişini kontrol edin.";
 
 /* */
-"Carbs" = "Carbs";
+"Carbs" = "Karbonhidrat";
 
 /* */
-"Temp Basal" = "Temp Basal";
+"Temp Basal" = "Geçici Bazal Oranı";
 
 /* */
-"Temp Target" = "Temp Target";
+"Temp Target" = "Geçici Hedef";
 
 /* */
-"Resume" = "Resume";
+"Resume" = "Devam et";
 
 /* */
-"Suspend" = "Suspend";
+"Suspend" = "Askıya al";
+
+/* */
+"Animated Background" = "Animasyonlu Arka Plan";
 
 
 /* Headers for settings ----------------------- */

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS main settings";

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

@@ -893,6 +893,9 @@ Enact a temp Basal or a temp target */
 /* */
 "Suspend" = "Suspend";
 
+/* */
+"Animated Background" = "Animated Background";
+
 
 /* Headers for settings ----------------------- */
 "OpenAPS main settings" = "OpenAPS 主要设置";

+ 17 - 0
FreeAPS/Sources/Models/BasalProfileEntry.swift

@@ -9,3 +9,20 @@ struct BasalProfileEntry: JSON, Equatable {
 protocol BasalProfileObserver {
     func basalProfileDidChange(_ basalProfile: [BasalProfileEntry])
 }
+
+extension BasalProfileEntry {
+    private enum CodingKeys: String, CodingKey {
+        case start
+        case minutes
+        case rate
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let start = try container.decode(String.self, forKey: .start)
+        let minutes = try container.decode(Int.self, forKey: .minutes)
+        let rate = try container.decode(Double.self, forKey: .rate).decimal ?? .zero
+
+        self = BasalProfileEntry(start: start, minutes: minutes, rate: rate)
+    }
+}

+ 17 - 0
FreeAPS/Sources/Models/CarbRatios.swift

@@ -15,3 +15,20 @@ enum CarbUnit: String, JSON {
     case grams
     case exchanges
 }
+
+extension CarbRatioEntry {
+    private enum CodingKeys: String, CodingKey {
+        case start
+        case offset
+        case ratio
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let start = try container.decode(String.self, forKey: .start)
+        let offset = try container.decode(Int.self, forKey: .offset)
+        let ratio = try container.decode(Double.self, forKey: .ratio).decimal ?? .zero
+
+        self = CarbRatioEntry(start: start, offset: offset, ratio: ratio)
+    }
+}

+ 17 - 0
FreeAPS/Sources/Models/InsulinSensitivities.swift

@@ -19,3 +19,20 @@ struct InsulinSensitivityEntry: JSON {
     let offset: Int
     let start: String
 }
+
+extension InsulinSensitivityEntry {
+    private enum CodingKeys: String, CodingKey {
+        case sensitivity
+        case offset
+        case start
+    }
+
+    init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        let sensitivity = try container.decode(Double.self, forKey: .sensitivity).decimal ?? .zero
+        let start = try container.decode(String.self, forKey: .start)
+        let offset = try container.decode(Int.self, forKey: .offset)
+
+        self = InsulinSensitivityEntry(sensitivity: sensitivity, offset: offset, start: start)
+    }
+}

+ 2 - 1
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -15,7 +15,8 @@ extension BasalProfileEditor {
         }
 
         override func subscribe() {
-            rateValues = provider.supportedBasalRates ?? stride(from: Decimal(0.05), to: 10.01, by: 0.05).map { $0 }
+            rateValues = provider.supportedBasalRates ?? stride(from: 5.0, to: 1001.0, by: 5.0)
+                .map { ($0.decimal ?? .zero) / 100 }
             items = provider.profile.map { value in
                 let timeIndex = timeValues.firstIndex(of: Double(value.minutes * 60)) ?? 0
                 let rateIndex = rateValues.firstIndex(of: value.rate) ?? 0

+ 1 - 1
FreeAPS/Sources/Modules/CREditor/CREditorStateModel.swift

@@ -7,7 +7,7 @@ extension CREditor {
 
         let timeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
 
-        let rateValues = stride(from: 3, to: 50.01, by: 0.1).map { Decimal($0) }
+        let rateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
 
         var canAdd: Bool {
             guard let lastItem = items.last else { return true }

+ 86 - 72
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -37,82 +37,100 @@ extension Home {
             return scene
         }
 
-        var header: some View {
+        @ViewBuilder func header(_ geo: GeometryProxy) -> some View {
             HStack(alignment: .bottom) {
                 Spacer()
-                VStack(alignment: .leading, spacing: 12) {
-                    HStack {
-                        Text("IOB").font(.caption2).foregroundColor(.secondary)
-                        Text(
-                            (numberFormatter.string(from: (state.suggestion?.iob ?? 0) as NSNumber) ?? "0") +
-                                NSLocalizedString(" U", comment: "Insulin unit")
-                        )
-                        .font(.system(size: 12, weight: .bold))
-                    }
-                    HStack {
-                        Text("COB").font(.caption2).foregroundColor(.secondary)
-                        Text(
-                            (numberFormatter.string(from: (state.suggestion?.cob ?? 0) as NSNumber) ?? "0") +
-                                NSLocalizedString(" g", comment: "gram of carbs")
-                        )
-                        .font(.system(size: 12, weight: .bold))
-                    }
-                }
+                cobIobView
+                Spacer()
+                glucoseView
+                Spacer()
+                pumpView
                 Spacer()
+                loopView
+                Spacer()
+            }
+            .frame(maxWidth: .infinity)
+            .frame(maxHeight: 70)
+            .padding(.top, geo.safeAreaInsets.top)
+            .background(Color.gray.opacity(0.2))
+        }
 
-                CurrentGlucoseView(
-                    recentGlucose: $state.recentGlucose,
-                    delta: $state.glucoseDelta,
-                    units: $state.units,
-                    alarm: $state.alarm
-                )
-                .onTapGesture {
-                    if state.alarm == nil {
-                        state.openCGM()
-                    } else {
-                        state.showModal(for: .snooze)
-                    }
+        var cobIobView: some View {
+            VStack(alignment: .leading, spacing: 12) {
+                HStack {
+                    Text("IOB").font(.caption2).foregroundColor(.secondary)
+                    Text(
+                        (numberFormatter.string(from: (state.suggestion?.iob ?? 0) as NSNumber) ?? "0") +
+                            NSLocalizedString(" U", comment: "Insulin unit")
+                    )
+                    .font(.system(size: 12, weight: .bold))
                 }
-                .onLongPressGesture {
-                    let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                    impactHeavy.impactOccurred()
-                    if state.alarm == nil {
-                        state.showModal(for: .snooze)
-                    } else {
-                        state.openCGM()
-                    }
+                HStack {
+                    Text("COB").font(.caption2).foregroundColor(.secondary)
+                    Text(
+                        (numberFormatter.string(from: (state.suggestion?.cob ?? 0) as NSNumber) ?? "0") +
+                            NSLocalizedString(" g", comment: "gram of carbs")
+                    )
+                    .font(.system(size: 12, weight: .bold))
                 }
+            }
+        }
 
-                Spacer()
-                PumpView(
-                    reservoir: $state.reservoir,
-                    battery: $state.battery,
-                    name: $state.pumpName,
-                    expiresAtDate: $state.pumpExpiresAtDate,
-                    timerDate: $state.timerDate
-                )
-                .onTapGesture {
-                    if state.pumpDisplayState != nil {
-                        state.setupPump = true
-                    }
+        var glucoseView: some View {
+            CurrentGlucoseView(
+                recentGlucose: $state.recentGlucose,
+                delta: $state.glucoseDelta,
+                units: $state.units,
+                alarm: $state.alarm
+            )
+            .onTapGesture {
+                if state.alarm == nil {
+                    state.openCGM()
+                } else {
+                    state.showModal(for: .snooze)
                 }
-                Spacer()
-                LoopView(
-                    suggestion: $state.suggestion,
-                    enactedSuggestion: $state.enactedSuggestion,
-                    closedLoop: $state.closedLoop,
-                    timerDate: $state.timerDate,
-                    isLooping: $state.isLooping,
-                    lastLoopDate: $state.lastLoopDate
-                ).onTapGesture {
-                    isStatusPopupPresented = true
-                }.onLongPressGesture {
-                    let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
-                    impactHeavy.impactOccurred()
-                    state.runLoop()
+            }
+            .onLongPressGesture {
+                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                impactHeavy.impactOccurred()
+                if state.alarm == nil {
+                    state.showModal(for: .snooze)
+                } else {
+                    state.openCGM()
                 }
-                Spacer()
-            }.frame(maxWidth: .infinity)
+            }
+        }
+
+        var pumpView: some View {
+            PumpView(
+                reservoir: $state.reservoir,
+                battery: $state.battery,
+                name: $state.pumpName,
+                expiresAtDate: $state.pumpExpiresAtDate,
+                timerDate: $state.timerDate
+            )
+            .onTapGesture {
+                if state.pumpDisplayState != nil {
+                    state.setupPump = true
+                }
+            }
+        }
+
+        var loopView: some View {
+            LoopView(
+                suggestion: $state.suggestion,
+                enactedSuggestion: $state.enactedSuggestion,
+                closedLoop: $state.closedLoop,
+                timerDate: $state.timerDate,
+                isLooping: $state.isLooping,
+                lastLoopDate: $state.lastLoopDate
+            ).onTapGesture {
+                isStatusPopupPresented = true
+            }.onLongPressGesture {
+                let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                impactHeavy.impactOccurred()
+                state.runLoop()
+            }
         }
 
         var infoPanal: some View {
@@ -330,11 +348,7 @@ extension Home {
         var body: some View {
             GeometryReader { geo in
                 VStack(spacing: 0) {
-                    header
-                        .frame(maxHeight: 70)
-                        .padding(.top, geo.safeAreaInsets.top)
-                        .background(Color.gray.opacity(0.2))
-
+                    header(geo)
                     infoPanal
                     mainChart
                     legendPanal

+ 1 - 1
FreeAPS/Sources/Modules/ISFEditor/ISFEditorStateModel.swift

@@ -14,7 +14,7 @@ extension ISFEditor {
             case .mgdL:
                 return stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
             case .mmolL:
-                return stride(from: 0.1, to: 30.01, by: 0.1).map { Decimal($0) }
+                return stride(from: 1.0, to: 301.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
             }
         }
 

+ 17 - 2
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -1,3 +1,4 @@
+import Combine
 import SwiftUI
 
 struct DecimalTextField: UIViewRepresentable {
@@ -62,8 +63,11 @@ struct DecimalTextField: UIViewRepresentable {
         return textfield
     }
 
-    func updateUIView(_ textField: UITextField, context _: Context) {
-        if value != 0 {
+    func updateUIView(_ textField: UITextField, context: Context) {
+        let coordinator = context.coordinator
+        if coordinator.isEditing {
+            coordinator.resetEditing()
+        } else if value != 0 {
             textField.text = formatter.string(for: value)
         }
     }
@@ -79,6 +83,15 @@ struct DecimalTextField: UIViewRepresentable {
             parent = textField
         }
 
+        private(set) var isEditing = false
+        private var editingCancellable: AnyCancellable?
+
+        func resetEditing() {
+            editingCancellable = Just(false)
+                .delay(for: 0.5, scheduler: DispatchQueue.main)
+                .weakAssign(to: \.isEditing, on: self)
+        }
+
         func textField(
             _ textField: UITextField,
             shouldChangeCharactersIn range: NSRange,
@@ -106,6 +119,7 @@ struct DecimalTextField: UIViewRepresentable {
 
                 // Set Value
                 let double = number.doubleValue
+                isEditing = true
                 parent.value = Decimal(double)
             }
 
@@ -118,6 +132,7 @@ struct DecimalTextField: UIViewRepresentable {
         ) {
             // Format value with formatter at End Editing
             textField.text = parent.formatter.string(for: parent.value)
+            isEditing = false
         }
     }
 }