polscm32 2 лет назад
Родитель
Сommit
db42824170

+ 8 - 12
FreeAPS.xcodeproj/project.pbxproj

@@ -9,7 +9,6 @@
 /* Begin PBXBuildFile section */
 		041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */; };
 		0437CE46C12535A56504EC19 /* SnoozeRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5822B15939E719628E9FF7C /* SnoozeRootView.swift */; };
-		0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */; };
 		0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */; };
 		0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */; };
 		17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */; };
@@ -269,6 +268,7 @@
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
 		581516A42BCED84A00BF67D7 /* DebuggingIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */; };
 		581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 581516A82BCEEDF800BF67D7 /* NSPredicates.swift */; };
+		58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58237D9D2BCF0A6B00A47A79 /* PopupView.swift */; };
 		587DA1F62B77F3DD00B28F8A /* SettingsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */; };
 		5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE53A13D26F101B332EFFC8 /* AddTempTargetProvider.swift */; };
 		5D16287A969E64D18CE40E44 /* PumpConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */; };
@@ -327,8 +327,7 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDF530D82B40F8AC002CAF43 /* LockScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */; };
-		BDFD165A2AE40438007F0DDA /* AlternativeBolusCalcRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */; };
-		BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */; };
+		BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFD16592AE40438007F0DDA /* BolusRootView.swift */; };
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
@@ -536,7 +535,6 @@
 /* Begin PBXFileReference section */
 		0274EE6439B1C3ED70730D41 /* PumpSettingsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorDataFlow.swift; sourceTree = "<group>"; };
 		0CA3E609094E064C99A4752C /* PreferencesEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorStateModel.swift; sourceTree = "<group>"; };
-		10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		12204445D7632AF09264A979 /* PreferencesEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PreferencesEditorDataFlow.swift; sourceTree = "<group>"; };
 		19012CDB291D2CB900FB8210 /* LoopStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopStats.swift; sourceTree = "<group>"; };
 		190EBCC329FF136900BA767D /* StatConfigDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigDataFlow.swift; sourceTree = "<group>"; };
@@ -834,6 +832,7 @@
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
 		581516A32BCED84A00BF67D7 /* DebuggingIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggingIdentifiers.swift; sourceTree = "<group>"; };
 		581516A82BCEEDF800BF67D7 /* NSPredicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPredicates.swift; sourceTree = "<group>"; };
+		58237D9D2BCF0A6B00A47A79 /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; };
 		587DA1F52B77F3DD00B28F8A /* SettingsRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsRowView.swift; sourceTree = "<group>"; };
 		5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetDataFlow.swift; sourceTree = "<group>"; };
 		5C018D1680307A31C9ED7120 /* CGMStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMStateModel.swift; sourceTree = "<group>"; };
@@ -893,8 +892,7 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BDF530D72B40F8AC002CAF43 /* LockScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenView.swift; sourceTree = "<group>"; };
-		BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlternativeBolusCalcRootView.swift; sourceTree = "<group>"; };
-		BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultBolusCalcRootView.swift; sourceTree = "<group>"; };
+		BDFD16592AE40438007F0DDA /* BolusRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusRootView.swift; sourceTree = "<group>"; };
 		BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BasalProfileEditorRootView.swift; sourceTree = "<group>"; };
 		C19984D62EFC0035A9E9644D /* BolusProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusProvider.swift; sourceTree = "<group>"; };
 		C377490C77661D75E8C50649 /* ManualTempBasalRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalRootView.swift; sourceTree = "<group>"; };
@@ -2189,9 +2187,8 @@
 		B9488883C59C31550E0B4CEC /* View */ = {
 			isa = PBXGroup;
 			children = (
-				10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */,
-				BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */,
-				BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */,
+				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
+				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2843,6 +2840,7 @@
 				190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */,
 				3811DEA925C9D88300A708ED /* AppearanceManager.swift in Sources */,
 				CE7950242997D81700FA576E /* CGMSettingsView.swift in Sources */,
+				58237D9E2BCF0A6B00A47A79 /* PopupView.swift in Sources */,
 				38D0B3D925EC07C400CB6E88 /* CarbsEntry.swift in Sources */,
 				38A9260525F012D8009E3739 /* CarbRatios.swift in Sources */,
 				38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */,
@@ -2872,7 +2870,7 @@
 				193F6CDD2A512C8F001240FD /* Loops.swift in Sources */,
 				38B4F3CB25E502E200E76A18 /* WeakObjectSet.swift in Sources */,
 				38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */,
-				BDFD165A2AE40438007F0DDA /* AlternativeBolusCalcRootView.swift in Sources */,
+				BDFD165A2AE40438007F0DDA /* BolusRootView.swift in Sources */,
 				38E98A2525F52C9300C0CED0 /* IssueReporter.swift in Sources */,
 				190EBCC429FF136900BA767D /* StatConfigDataFlow.swift in Sources */,
 				3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */,
@@ -2994,9 +2992,7 @@
 				38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */,
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
-				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
-				BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */,
 				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,

+ 11 - 7
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -91,6 +91,7 @@ extension Bolus {
         @Published var skipBolus: Bool = false
 
         @Published var externalInsulin: Bool = false
+        @Published var showInfo: Bool = false
 
         let now = Date.now
 
@@ -237,8 +238,12 @@ extension Bolus {
             insulinCalculated = max(insulinCalculated, 0)
             insulinCalculated = min(insulinCalculated, maxBolus)
 
-            return apsManager
-                .roundBolus(amount: max(insulinCalculated, 0))
+            guard let apsManager = apsManager else {
+                debug(.apsManager, "APSManager could not be gracefully unwrapped")
+                return insulinCalculated
+            }
+
+            return apsManager.roundBolus(amount: insulinCalculated)
         }
 
         func setupInsulinRequired() {
@@ -355,7 +360,7 @@ extension Bolus {
             newItem.date = Date()
             newItem.external = false
             newItem.isSMB = false
-            self.context.perform {
+            context.perform {
                 do {
                     try self.context.save()
                     debugPrint(
@@ -366,7 +371,6 @@ extension Bolus {
                         "Bolus State: \(CoreDataStack.identifier) \(DebuggingIdentifiers.failed) failed to save pump insulin to core data"
                     )
                 }
-
             }
         }
 
@@ -418,9 +422,9 @@ extension Bolus {
             debug(.default, "External insulin saved to pumphistory.json")
 
             // save to core data asynchronously
-            self.context.perform {
+            context.perform {
                 let newItem = InsulinStored(context: self.context)
-                newItem.amount = (self.amount) as NSDecimalNumber
+                newItem.amount = self.amount as NSDecimalNumber
                 newItem.date = Date()
                 newItem.external = true
                 newItem.isSMB = false
@@ -435,7 +439,7 @@ extension Bolus {
                     )
                 }
             }
-            
+
             // perform determine basal sync
             apsManager.determineBasalSync()
         }

Разница между файлами не показана из-за своего большого размера
+ 0 - 1041
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift


+ 584 - 15
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -1,29 +1,598 @@
+import Charts
+import CoreData
+import LoopKitUI
 import SwiftUI
 import Swinject
 
 extension Bolus {
-    struct RootView: View {
+    struct RootView: BaseView {
         let resolver: Resolver
-        let waitForSuggestion: Bool
-        let fetch: Bool
+
         @StateObject var state = StateModel()
 
+        @State private var showAlert = false
+        @State private var autofocus: Bool = true
+        @State private var calculatorDetent = PresentationDetent.medium
+        @State private var pushed: Bool = false
+        @State private var isPromptPresented: Bool = false
+        @State private var dish: String = ""
+        @State private var saved: Bool = false
+        @State private var debounce: DispatchWorkItem?
+
+        @Environment(\.managedObjectContext) var moc
+
+        private enum Config {
+            static let dividerHeight: CGFloat = 2
+            static let spacing: CGFloat = 3
+        }
+
+        @Environment(\.colorScheme) var colorScheme
+
+        @FetchRequest(
+            entity: Presets.entity(),
+            sortDescriptors: [NSSortDescriptor(key: "dish", ascending: true)]
+        ) var carbPresets: FetchedResults<Presets>
+
+        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 }
+        }
+
+        private var color: LinearGradient {
+            colorScheme == .dark ? LinearGradient(
+                gradient: Gradient(colors: [
+                    Color.bgDarkBlue,
+                    Color.bgDarkerDarkBlue
+                ]),
+                startPoint: .top,
+                endPoint: .bottom
+            )
+                :
+                LinearGradient(
+                    gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
+                    startPoint: .top,
+                    endPoint: .bottom
+                )
+        }
+
+        private var empty: Bool {
+            state.useFPUconversion ? (state.carbs <= 0 && state.fat <= 0 && state.protein <= 0) : (state.carbs <= 0)
+        }
+
+        /// Handles macro input (carb, fat, protein) in a debounced fashion.
+        func handleDebouncedInput() {
+            debounce?.cancel()
+            debounce = DispatchWorkItem { [self] in
+                state.insulinCalculated = state.calculateInsulin()
+            }
+            if let debounce = debounce {
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: debounce)
+            }
+        }
+
+        private var presetPopover: some View {
+            Form {
+                Section {
+                    TextField("Name Of Dish", text: $dish)
+                    Button {
+                        saved = true
+                        if dish != "", saved {
+                            let preset = Presets(context: moc)
+                            preset.dish = dish
+                            preset.fat = state.fat as NSDecimalNumber
+                            preset.protein = state.protein as NSDecimalNumber
+                            preset.carbs = state.carbs as NSDecimalNumber
+                            try? moc.save()
+                            state.addNewPresetToWaitersNotepad(dish)
+                            saved = false
+                            isPromptPresented = false
+                        }
+                    }
+                    label: { Text("Save") }
+                    Button {
+                        dish = ""
+                        saved = false
+                        isPromptPresented = false }
+                    label: { Text("Cancel") }
+                } header: { Text("Enter Meal Preset Name") }
+            }
+        }
+
+        private var minusButton: some View {
+            Button {
+                if state.carbs != 0,
+                   (state.carbs - (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+                {
+                    state.carbs -= (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal)
+                } else { state.carbs = 0 }
+
+                if state.fat != 0,
+                   (state.fat - (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+                {
+                    state.fat -= (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal)
+                } else { state.fat = 0 }
+
+                if state.protein != 0,
+                   (state.protein - (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) as Decimal) >= 0
+                {
+                    state.protein -= (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal)
+                } else { state.protein = 0 }
+
+                state.removePresetFromNewMeal()
+                if state.carbs == 0, state.fat == 0, state.protein == 0 { state.summation = [] }
+            }
+            label: { Image(systemName: "minus.circle.fill")
+                .font(.system(size: 20))
+            }
+            .disabled(
+                state
+                    .selection == nil ||
+                    (
+                        !state.summation
+                            .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
+                    )
+            )
+            .buttonStyle(.borderless)
+            .tint(.blue)
+        }
+
+        private var plusButton: some View {
+            Button {
+                state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
+                state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
+                state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+
+                state.addPresetToNewMeal()
+            }
+            label: { Image(systemName: "plus.circle.fill")
+                .font(.system(size: 20))
+            }
+            .disabled(state.selection == nil)
+            .buttonStyle(.borderless)
+            .tint(.blue)
+        }
+
+        private var mealPresets: some View {
+            Section {
+                HStack {
+                    if state.selection != nil {
+                        minusButton
+                    }
+                    Picker("Preset", selection: $state.selection) {
+                        Text("Saved Food").tag(nil as Presets?)
+                        ForEach(carbPresets, id: \.self) { (preset: Presets) in
+                            Text(preset.dish ?? "").tag(preset as Presets?)
+                        }
+                    }
+                    .labelsHidden()
+                    .frame(maxWidth: .infinity, alignment: .center)
+                    ._onBindingChange($state.selection) { _ in
+                        state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
+                        state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
+                        state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+                        state.addToSummation()
+                    }
+                    if state.selection != nil {
+                        plusButton
+                    }
+                }
+
+                HStack {
+                    Button("Delete Preset") {
+                        showAlert.toggle()
+                    }
+                    .disabled(state.selection == nil)
+                    .tint(.orange)
+                    .buttonStyle(.borderless)
+                    .alert(
+                        "Delete preset '\(state.selection?.dish ?? "")'?",
+                        isPresented: $showAlert,
+                        actions: {
+                            Button("No", role: .cancel) {}
+                            Button("Yes", role: .destructive) {
+                                state.deletePreset()
+
+                                state.carbs += ((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal
+                                state.fat += ((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal
+                                state.protein += ((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal
+
+                                state.addPresetToNewMeal()
+                            }
+                        }
+                    )
+
+                    Spacer()
+
+                    Button {
+                        isPromptPresented = true
+                    }
+                    label: { Text("Save as Preset") }
+                        .buttonStyle(.borderless)
+                        .disabled(
+                            empty ||
+                                (
+                                    (((state.selection?.carbs ?? 0) as NSDecimalNumber) as Decimal) == state
+                                        .carbs && (((state.selection?.fat ?? 0) as NSDecimalNumber) as Decimal) == state
+                                        .fat && (((state.selection?.protein ?? 0) as NSDecimalNumber) as Decimal) == state
+                                        .protein
+                                )
+                        )
+                }
+            }
+        }
+
+        @ViewBuilder private func proteinAndFat() -> some View {
+            HStack {
+                Text("Fat").foregroundColor(.orange)
+                Spacer()
+                DecimalTextField(
+                    "0",
+                    value: $state.fat,
+                    formatter: formatter,
+                    autofocus: false,
+                    cleanInput: true
+                )
+                Text("g").foregroundColor(.secondary)
+            }
+            HStack {
+                Text("Protein").foregroundColor(.red)
+                Spacer()
+                DecimalTextField(
+                    "0",
+                    value: $state.protein,
+                    formatter: formatter,
+                    autofocus: false,
+                    cleanInput: true
+                ).foregroundColor(.loopRed)
+
+                Text("g").foregroundColor(.secondary)
+            }
+        }
+
         var body: some View {
-            if state.useCalc {
-                // show alternative bolus calc based on toggle in bolus calc settings
-                AlternativeBolusCalcRootView(
-                    resolver: resolver,
-                    state: state
+            ZStack(alignment: .center) {
+                VStack {
+                    Form {
+                        Section {
+                            HStack {
+                                Text("Carbs").fontWeight(.semibold)
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.carbs,
+                                    formatter: formatter,
+                                    autofocus: false,
+                                    cleanInput: true
+                                ).onChange(of: state.carbs) { _ in
+                                    if state.carbs > 0 {
+                                        handleDebouncedInput()
+                                    }
+                                }
+                                Text("g").foregroundColor(.secondary)
+                            }
+
+                            if state.useFPUconversion {
+                                proteinAndFat()
+                            }
+
+                            // Summary when combining presets
+                            if state.waitersNotepad() != "" {
+                                HStack {
+                                    Text("Total")
+                                    let test = state.waitersNotepad().components(separatedBy: ", ").removeDublicates()
+                                    HStack(spacing: 0) {
+                                        ForEach(test, id: \.self) {
+                                            Text($0).foregroundStyle(Color.randomGreen()).font(.footnote)
+                                            Text($0 == test[test.count - 1] ? "" : ", ")
+                                        }
+                                    }.frame(maxWidth: .infinity, alignment: .trailing)
+                                }
+                            }
+
+                            // Time
+                            HStack {
+                                Text("Time").foregroundStyle(Color.secondary)
+                                Spacer()
+                                if !pushed {
+                                    Button {
+                                        pushed = true
+                                    } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary)
+                                        .padding(.trailing, 5)
+                                } else {
+                                    Button { state.date = state.date.addingTimeInterval(-15.minutes.timeInterval) }
+                                    label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+                                    DatePicker(
+                                        "Time",
+                                        selection: $state.date,
+                                        displayedComponents: [.hourAndMinute]
+                                    ).controlSize(.mini)
+                                        .labelsHidden()
+                                    Button {
+                                        state.date = state.date.addingTimeInterval(15.minutes.timeInterval)
+                                    }
+                                    label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
+                                }
+                            }
+
+                            .popover(isPresented: $isPromptPresented) {
+                                presetPopover
+                            }
+                        }.listRowBackground(Color.chart)
+
+                        if state.displayPresets {
+                            Section {
+                                mealPresets
+                            }.listRowBackground(Color.chart)
+                        }
+
+                        Section {
+                            HStack {
+                                Button(action: {
+                                    state.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.useFattyMealCorrectionFactor {
+                                            state.useSuperBolus = false
+                                        }
+                                    }
+                                }
+                                if state.sweetMeals {
+                                    Spacer()
+                                    Toggle(isOn: $state.useSuperBolus) {
+                                        Text("Super Bolus")
+                                    }
+                                    .toggleStyle(CheckboxToggleStyle())
+                                    .font(.footnote)
+                                    .onChange(of: state.useSuperBolus) { _ in
+                                        state.insulinCalculated = state.calculateInsulin()
+                                        if state.useSuperBolus {
+                                            state.useFattyMealCorrectionFactor = false
+                                        }
+                                    }
+                                }
+                            }
+
+                            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 }
+
+                            HStack {
+                                Text("Bolus")
+                                Spacer()
+                                DecimalTextField(
+                                    "0",
+                                    value: $state.amount,
+                                    formatter: formatter,
+                                    autofocus: false,
+                                    cleanInput: true,
+                                    textColor: .systemBlue
+                                )
+                                Text(" U").foregroundColor(.secondary)
+                            }
+
+                            if state.amount > 0 {
+                                HStack {
+                                    Text("External insulin")
+                                    Spacer()
+                                    Toggle("", isOn: $state.externalInsulin).toggleStyle(Checkbox())
+                                }
+                            }
+                        }.listRowBackground(Color.chart)
+                    }
+                }.safeAreaInset(edge: .bottom, spacing: 0) {
+                    stickyButton
+                }.blur(radius: state.waitForSuggestion ? 5 : 0)
+
+                if state.waitForSuggestion {
+                    CustomProgressView(text: progressText.rawValue)
+                }
+            }
+            .scrollContentBackground(.hidden).background(color)
+            .blur(radius: state.showInfo ? 3 : 0)
+            .navigationTitle("Treatments")
+            .navigationBarTitleDisplayMode(.inline)
+            .toolbar(content: {
+                ToolbarItem(placement: .topBarLeading) {
+                    Button {
+                        state.hideModal()
+                    } label: {
+                        Text("Close")
+                    }
+                }
+            })
+            .onAppear {
+                configureView {
+                    state.insulinCalculated = state.calculateInsulin()
+                }
+            }
+            .onDisappear {
+                state.addButtonPressed = false
+            }
+            .sheet(isPresented: $state.showInfo) {
+                PopupView(state: state)
+                    .presentationDetents(
+                        [.fraction(0.9), .large],
+                        selection: $calculatorDetent
+                    )
+            }
+        }
+
+        var progressText: ProgressText {
+            switch (state.amount > 0, state.carbs > 0) {
+            case (true, true):
+                return .updatingIOBandCOB
+            case (false, true):
+                return .updatingCOB
+            case (true, false):
+                return .updatingIOB
+            default:
+                return .updatingTreatments
+            }
+        }
+
+        var stickyButton: some View {
+            ZStack {
+                Rectangle()
+                    .frame(width: UIScreen.main.bounds.width, height: 120).offset(y: 40)
+                    .shadow(
+                        color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
+                            Color.black.opacity(0.33),
+                        radius: 3
+                    )
+                    .foregroundStyle(Color.chart)
+
+                Button {
+                    state.invokeTreatmentsTask()
+                } label: {
+                    taskButtonLabel
+                        .font(.headline)
+                        .foregroundStyle(Color.white)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .frame(minHeight: 50)
+                }
+                .disabled(disableTaskButton)
+                .background(
+                    (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) ? Color(.systemRed) :
+                        Color(.systemBlue)
+                )
+                .shadow(radius: 3)
+                .clipShape(RoundedRectangle(cornerRadius: 8))
+                .padding()
+                .offset(y: 20)
+            }
+        }
+
+        private var taskButtonLabel: some View {
+            let hasInsulin = state.amount > 0
+            let hasCarbs = state.carbs > 0
+            let hasFatOrProtein = state.fat > 0 || state.protein > 0
+
+            switch (hasInsulin, hasCarbs, hasFatOrProtein) {
+            case (true, true, true):
+                return Text(
+                    state
+                        .externalInsulin ? (
+                            externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log meal and external insulin"
+                        ) :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log meal and enact bolus")
+                )
+            case (true, true, false):
+                return Text(
+                    state
+                        .externalInsulin ?
+                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log carbs and external insulin") :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log carbs and enact bolus")
+                )
+            case (true, false, true):
+                return Text(
+                    state
+                        .externalInsulin ?
+                        (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log FPUs and external insulin") :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Log FPUs and enact bolus")
                 )
-            } else {
-                // show iAPS standard bolus calc
-                DefaultBolusCalcRootView(
-                    resolver: resolver,
-                    waitForSuggestion: waitForSuggestion,
-                    fetch: fetch,
-                    state: state
+            case (true, false, false):
+                return Text(
+                    state
+                        .externalInsulin ? (externalBolusLimit ? "Manual bolus exceeds max bolus!" : "Log external insulin") :
+                        (pumpBolusLimit ? "Pump bolus exceeds max bolus!" : "Enact bolus")
                 )
+            case (false, true, true):
+                return Text("Log meal")
+            case (false, true, false):
+                return Text("Log carbs")
+            case (false, false, true):
+                return Text("Log FPUs")
+            default:
+                return Text("Continue without treatment")
+            }
+        }
+
+        private var pumpBolusLimit: Bool {
+            state.amount > state.maxBolus
+        }
+
+        private var externalBolusLimit: Bool {
+            state.amount > state.maxBolus * 3
+        }
+
+        private var disableTaskButton: Bool {
+            state.amount > 0 ? (state.externalInsulin ? externalBolusLimit : pumpBolusLimit) : false
+        }
+    }
+
+    struct DividerDouble: View {
+        var body: some View {
+            VStack(spacing: 2) {
+                Rectangle()
+                    .frame(height: 1)
+                    .foregroundColor(.gray.opacity(0.65))
+                Rectangle()
+                    .frame(height: 1)
+                    .foregroundColor(.gray.opacity(0.65))
             }
+            .frame(height: 4)
+            .padding(.vertical)
+        }
+    }
+
+    struct DividerCustom: View {
+        var body: some View {
+            Rectangle()
+                .frame(height: 1)
+                .foregroundColor(.gray.opacity(0.65))
+                .padding(.vertical)
         }
     }
 }

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

@@ -1,376 +0,0 @@
-import Charts
-import CoreData
-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 }
-        }
-
-        private var color: LinearGradient {
-            colorScheme == .dark ? LinearGradient(
-                gradient: Gradient(colors: [
-                    Color.bgDarkBlue,
-                    Color.bgDarkerDarkBlue
-                ]),
-                startPoint: .top,
-                endPoint: .bottom
-            )
-                :
-                LinearGradient(
-                    gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
-                    startPoint: .top,
-                    endPoint: .bottom
-                )
-        }
-
-        var body: some View {
-            Form {
-                Section {
-                    if state.waitForSuggestion {
-                        Text("Please wait")
-                    }
-                } header: { Text("Predictions") }.listRowBackground(Color.chart)
-
-                if fetch {
-                    Section {
-                        mealEntries
-                    } header: { Text("Meal Summary") }.listRowBackground(Color.chart)
-                }
-
-                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") }.listRowBackground(Color.chart)
-
-                if state.amount > 0 {
-                    Section {
-                        Button {
-                            Task {
-//                                add()
-                                state.hideModal()
-                                keepForNextWiew = true
-                            }
-                        }
-                        label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
-                            .frame(maxWidth: .infinity, alignment: .center)
-                            .disabled(disabled)
-                            .listRowBackground(!disabled ? Color(.systemBlue) : Color(.systemGray4))
-                            .tint(.white)
-                    }
-                }
-
-                if state.amount <= 0 {
-                    Section {
-                        Button {
-                            keepForNextWiew = true
-                            state.hideModal()
-                        }
-                        label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
-                    }.listRowBackground(Color.chart)
-                }
-            }.scrollContentBackground(.hidden).background(color)
-                .alert(isPresented: $displayError) {
-                    Alert(
-                        title: Text("Warning!"),
-                        message: Text("\n" + alertString() + "\n"),
-                        primaryButton: .destructive(
-                            Text("Add"),
-                            action: {
-                                state.amount = state.insulinRecommended
-                                displayError = false
-                            }
-                        ),
-                        secondaryButton: .cancel()
-                    )
-                }.onAppear {
-                    configureView {
-                        state.waitForSuggestionInitial = waitForSuggestion
-                        state.waitForSuggestion = waitForSuggestion
-                    }
-                }
-                .navigationTitle("Enact Bolus")
-                .navigationBarTitleDisplayMode(.inline)
-                .toolbar {
-                    ToolbarItem(placement: .topBarLeading) {
-                        Button {
-                            state.hideModal()
-                        } label: {
-                            Text("Close")
-                        }
-                    }
-                }
-                .popup(isPresented: presentInfo, alignment: .center, direction: .bottom) {
-                    bolusInfo
-                }
-        }
-
-        var disabled: Bool {
-            state.amount <= 0 || state.amount > state.maxBolus
-        }
-
-        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 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..."
-            }
-        }
-    }
-}

+ 482 - 0
FreeAPS/Sources/Modules/Bolus/View/PopupView.swift

@@ -0,0 +1,482 @@
+import SwiftUI
+
+struct PopupView: View {
+    @StateObject var state: Bolus.StateModel
+    @Environment(\.colorScheme) var colorScheme
+
+    private var fractionDigits: Int {
+        if state.units == .mmolL {
+            return 1
+        } else { return 0 }
+    }
+
+    var body: some View {
+        NavigationStack {
+            ScrollView {
+                Grid(alignment: .topLeading, horizontalSpacing: 3, verticalSpacing: 0) {
+                    GridRow {
+                        Text("Calculations").fontWeight(.bold).gridCellColumns(3).gridCellAnchor(.center).padding(.vertical)
+                    }
+
+                    calcSettingsFirstRow
+                    calcSettingsSecondRow
+
+                    DividerCustom()
+
+                    // meal entries as grid rows
+                    if state.carbs > 0 {
+                        GridRow {
+                            Text("Carbs").foregroundColor(.secondary)
+                            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+                            HStack {
+                                Text(state.carbs.formatted())
+                                Text("g").foregroundColor(.secondary)
+                            }.gridCellAnchor(.trailing)
+                        }
+                    }
+
+                    if state.fat > 0 {
+                        GridRow {
+                            Text("Fat").foregroundColor(.secondary)
+                            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+                            HStack {
+                                Text(state.fat.formatted())
+                                Text("g").foregroundColor(.secondary)
+                            }.gridCellAnchor(.trailing)
+                        }
+                    }
+
+                    if state.protein > 0 {
+                        GridRow {
+                            Text("Protein").foregroundColor(.secondary)
+                            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+                            HStack {
+                                Text(state.protein.formatted())
+                                Text("g").foregroundColor(.secondary)
+                            }.gridCellAnchor(.trailing)
+                        }
+                    }
+
+                    if state.carbs > 0 || state.protein > 0 || state.fat > 0 {
+                        DividerCustom()
+                    }
+
+                    GridRow {
+                        Text("Detailed Calculation Steps").gridCellColumns(3).gridCellAnchor(.center)
+                            .padding(.bottom, 10)
+                    }
+                    calcGlucoseFirstRow
+                    calcGlucoseSecondRow.padding(.bottom, 5)
+                    calcGlucoseFormulaRow
+
+                    DividerCustom()
+
+                    calcIOBRow
+
+                    DividerCustom()
+
+                    calcCOBRow.padding(.bottom, 5)
+                    calcCOBFormulaRow
+
+                    DividerCustom()
+
+                    calcDeltaRow
+                    calcDeltaFormulaRow
+
+                    DividerCustom()
+
+                    calcFullBolusRow
+
+                    if state.useSuperBolus {
+                        DividerCustom()
+                        calcSuperBolusRow
+                    }
+
+                    DividerDouble()
+
+                    calcResultRow
+                    calcResultFormulaRow
+                }
+
+                Spacer()
+
+                Button { state.showInfo = false }
+                label: { Text("Got it!").frame(maxWidth: .infinity, alignment: .center) }
+                    .buttonStyle(.bordered)
+                    .padding(.top)
+            }
+            .padding([.horizontal, .bottom])
+            .font(.subheadline)
+        }
+    }
+
+    var calcSettingsFirstRow: some View {
+        GridRow {
+            Group {
+                Text("Carb Ratio:")
+                    .foregroundColor(.secondary)
+            }.gridCellAnchor(.leading)
+
+            Group {
+                Text("ISF:")
+                    .foregroundColor(.secondary)
+            }.gridCellAnchor(.leading)
+
+            VStack {
+                Text("Target:")
+                    .foregroundColor(.secondary)
+            }.gridCellAnchor(.leading)
+        }
+    }
+
+    var calcSettingsSecondRow: some View {
+        GridRow {
+            Text(state.carbRatio.formatted() + " " + NSLocalizedString("g/U", comment: " grams per Unit"))
+                .gridCellAnchor(.leading)
+
+            Text(
+                state.isf.formatted() + " " + state.units
+                    .rawValue + NSLocalizedString("/U", comment: "/Insulin unit")
+            ).gridCellAnchor(.leading)
+            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+            Text(
+                target
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    " " + state.units.rawValue
+            ).gridCellAnchor(.leading)
+        }
+    }
+
+    var calcGlucoseFirstRow: some View {
+        GridRow(alignment: .center) {
+            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+            let target = state.units == .mmolL ? state.target.asMmolL : state.target
+
+            Text("Glucose:").foregroundColor(.secondary)
+
+            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
+            let firstRow = currentBG
+                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+
+                + " - " +
+                target
+                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+                + " = " +
+                targetDifference
+                .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+
+            Text(firstRow).frame(minWidth: 0, alignment: .leading).foregroundColor(.secondary)
+                .gridColumnAlignment(.leading)
+
+            HStack {
+                Text(
+                    self.insulinRounder(state.targetDifferenceInsulin).formatted()
+                )
+                Text("U").foregroundColor(.secondary)
+            }.fontWeight(.bold)
+                .gridColumnAlignment(.trailing)
+        }
+    }
+
+    var calcGlucoseSecondRow: some View {
+        GridRow(alignment: .center) {
+            let currentBG = state.units == .mmolL ? state.currentBG.asMmolL : state.currentBG
+            Text(
+                currentBG
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                    " " +
+                    state.units.rawValue
+            )
+
+            let targetDifference = state.units == .mmolL ? state.targetDifference.asMmolL : state.targetDifference
+            let secondRow = targetDifference
+                .formatted(
+                    .number.grouping(.never).rounded()
+                        .precision(.fractionLength(fractionDigits))
+                )
+                + " / " +
+                state.isf.formatted()
+                + " ≈ " +
+                self.insulinRounder(state.targetDifferenceInsulin).formatted()
+
+            Text(secondRow).foregroundColor(.secondary).gridColumnAlignment(.leading)
+
+            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+        }
+    }
+
+    var calcGlucoseFormulaRow: some View {
+        GridRow(alignment: .top) {
+            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+
+            Text("(Current - Target) / ISF").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                .gridColumnAlignment(.leading)
+                .gridCellColumns(2)
+        }
+        .font(.caption)
+    }
+
+    var calcIOBRow: some View {
+        GridRow(alignment: .center) {
+            HStack {
+                Text("IOB:").foregroundColor(.secondary)
+                Text(
+                    self.insulinRounder(state.iob).formatted()
+                )
+            }
+
+            Text("Subtract IOB").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
+
+            let iobFormatted = self.insulinRounder(state.iob).formatted()
+            HStack {
+                Text((state.iob >= 0 ? "-" : "") + (state.iob >= 0 ? iobFormatted : "(" + iobFormatted + ")"))
+                Text("U").foregroundColor(.secondary)
+            }.fontWeight(.bold)
+                .gridColumnAlignment(.trailing)
+        }
+    }
+
+    var calcCOBRow: some View {
+        GridRow(alignment: .center) {
+            HStack {
+                Text("COB:").foregroundColor(.secondary)
+                Text(
+                    state.wholeCob
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits))) +
+                        NSLocalizedString(" g", comment: "grams")
+                )
+            }
+
+            Text(
+                state.wholeCob
+                    .formatted(.number.grouping(.never).rounded().precision(.fractionLength(fractionDigits)))
+                    + " / " +
+                    state.carbRatio.formatted()
+                    + " ≈ " +
+                    self.insulinRounder(state.wholeCobInsulin).formatted()
+            )
+            .foregroundColor(.secondary)
+            .gridColumnAlignment(.leading)
+
+            HStack {
+                Text(
+                    self.insulinRounder(state.wholeCobInsulin).formatted()
+                )
+                Text("U").foregroundColor(.secondary)
+            }.fontWeight(.bold)
+                .gridColumnAlignment(.trailing)
+        }
+    }
+
+    var calcCOBFormulaRow: some View {
+        GridRow(alignment: .center) {
+            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+
+            Text("COB / Carb Ratio").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                .gridColumnAlignment(.leading)
+                .gridCellColumns(2)
+        }
+        .font(.caption)
+    }
+
+    var calcDeltaRow: some View {
+        GridRow(alignment: .center) {
+            Text("Delta:").foregroundColor(.secondary)
+
+            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            Text(
+                deltaBG
+                    .formatted(
+                        .number.grouping(.never).rounded()
+                            .precision(.fractionLength(fractionDigits))
+                    )
+                    + " / " +
+                    state.isf.formatted()
+                    + " ≈ " +
+                    self.insulinRounder(state.fifteenMinInsulin).formatted()
+            )
+            .foregroundColor(.secondary)
+            .gridColumnAlignment(.leading)
+
+            HStack {
+                Text(
+                    self.insulinRounder(state.fifteenMinInsulin).formatted()
+                )
+                Text("U").foregroundColor(.secondary)
+            }.fontWeight(.bold)
+                .gridColumnAlignment(.trailing)
+        }
+    }
+
+    var calcDeltaFormulaRow: some View {
+        GridRow(alignment: .center) {
+            let deltaBG = state.units == .mmolL ? state.deltaBG.asMmolL : state.deltaBG
+            Text(
+                deltaBG
+                    .formatted(
+                        .number.grouping(.never).rounded()
+                            .precision(.fractionLength(fractionDigits))
+                    ) + " " +
+                    state.units.rawValue
+            )
+
+            Text("15min Delta / ISF").font(.caption).foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                .gridColumnAlignment(.leading)
+                .gridCellColumns(2).padding(.top, 5)
+        }
+    }
+
+    var calcFullBolusRow: some View {
+        GridRow(alignment: .center) {
+            Text("Full Bolus")
+                .foregroundColor(.secondary)
+
+            Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+
+            HStack {
+                Text(self.insulinRounder(state.wholeCalc).formatted())
+                    .foregroundStyle(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
+                Text("U").foregroundColor(.secondary)
+            }.gridColumnAlignment(.trailing)
+                .fontWeight(.bold)
+        }
+    }
+
+    var calcSuperBolusRow: some View {
+        GridRow(alignment: .center) {
+            Text("Super Bolus")
+                .foregroundColor(.secondary)
+
+            Text("Added to Result").foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8)).font(.footnote)
+
+            HStack {
+                Text("+" + self.insulinRounder(state.superBolusInsulin).formatted())
+                    .foregroundStyle(Color.loopRed)
+                Text("U").foregroundColor(.secondary)
+            }.gridColumnAlignment(.trailing)
+                .fontWeight(.bold)
+        }
+    }
+
+    var calcResultRow: some View {
+        GridRow(alignment: .center) {
+            Text("Result").fontWeight(.bold)
+
+            HStack {
+                Text(state.useSuperBolus ? "(" : "")
+                    .foregroundColor(.loopRed)
+
+                    + Text(state.fraction.formatted())
+
+                    + Text(" x ")
+                    .foregroundColor(.secondary)
+
+                    // if fatty meal is chosen
+                    + Text(state.useFattyMealCorrectionFactor ? state.fattyMealFactor.formatted() : "")
+                    .foregroundColor(.orange)
+
+                    + Text(state.useFattyMealCorrectionFactor ? " x " : "")
+                    .foregroundColor(.secondary)
+                    // endif fatty meal is chosen
+
+                    + Text(self.insulinRounder(state.wholeCalc).formatted())
+                    .foregroundColor(state.wholeCalc < 0 ? Color.loopRed : Color.primary)
+
+                    // if superbolus is chosen
+                    + Text(state.useSuperBolus ? ")" : "")
+                    .foregroundColor(.loopRed)
+
+                    + Text(state.useSuperBolus ? " + " : "")
+                    .foregroundColor(.secondary)
+
+                    + Text(state.useSuperBolus ? state.superBolusInsulin.formatted() : "")
+                    .foregroundColor(.loopRed)
+                    // endif superbolus is chosen
+
+                    + Text(" ≈ ")
+                    .foregroundColor(.secondary)
+            }
+            .gridColumnAlignment(.leading)
+
+            HStack {
+                Text(self.insulinRounder(state.insulinCalculated).formatted())
+                    .fontWeight(.bold)
+                    .foregroundColor(state.wholeCalc >= state.maxBolus ? Color.loopRed : Color.blue)
+                Text("U").foregroundColor(.secondary)
+            }
+            .gridColumnAlignment(.trailing)
+            .fontWeight(.bold)
+        }
+    }
+
+    var calcResultFormulaRow: some View {
+        GridRow(alignment: .bottom) {
+            if state.useFattyMealCorrectionFactor {
+                Group {
+                    Text("Factor x Fatty Meal Factor x Full Bolus")
+                        .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                        +
+                        Text(state.wholeCalc > state.maxBolus ? " ≈ Max Bolus" : "").foregroundColor(Color.loopRed)
+                }
+                .font(.caption)
+                .gridCellAnchor(.center)
+                .gridCellColumns(3)
+            } else if state.useSuperBolus {
+                Group {
+                    Text("(Factor x Full Bolus) + Super Bolus")
+                        .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                        +
+                        Text(state.wholeCalc > state.maxBolus ? " ≈ Max Bolus" : "").foregroundColor(Color.loopRed)
+                }
+                .font(.caption)
+                .gridCellAnchor(.center)
+                .gridCellColumns(3)
+            } else {
+                Color.clear.gridCellUnsizedAxes([.horizontal, .vertical])
+                Group {
+                    Text("Factor x Full Bolus")
+                        .foregroundColor(.secondary.opacity(colorScheme == .dark ? 0.65 : 0.8))
+                        +
+                        Text(state.wholeCalc > state.maxBolus ? " ≈ Max Bolus" : "").foregroundColor(Color.loopRed)
+                }
+                .font(.caption)
+                .padding(.top, 5)
+                .gridCellAnchor(.leading)
+                .gridCellColumns(2)
+            }
+        }
+    }
+
+    private func insulinRounder(_ value: Decimal) -> Decimal {
+        let toRound = NSDecimalNumber(decimal: value).doubleValue
+        return Decimal(floor(100 * toRound) / 100)
+    }
+
+    struct DividerDouble: View {
+        var body: some View {
+            VStack(spacing: 2) {
+                Rectangle()
+                    .frame(height: 1)
+                    .foregroundColor(.gray.opacity(0.65))
+                Rectangle()
+                    .frame(height: 1)
+                    .foregroundColor(.gray.opacity(0.65))
+            }
+            .frame(height: 4)
+            .padding(.vertical)
+        }
+    }
+
+    struct DividerCustom: View {
+        var body: some View {
+            Rectangle()
+                .frame(height: 1)
+                .foregroundColor(.gray.opacity(0.65))
+                .padding(.vertical)
+        }
+    }
+}
+
+// #Preview {
+//    PopupView()
+// }

+ 1 - 1
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -817,7 +817,7 @@ extension Home {
 
                 Button(
                     action: {
-                        state.showModal(for: .bolus(waitForSuggestion: false, fetch: false)) },
+                        state.showModal(for: .bolus) },
                     label: {
                         Image(systemName: "plus.circle.fill")
                             .font(.system(size: 40))

+ 3 - 7
FreeAPS/Sources/Router/Screen.swift

@@ -15,7 +15,7 @@ enum Screen: Identifiable, Hashable {
     case targetsEditor
     case preferencesEditor
     case addTempTarget
-    case bolus(waitForSuggestion: Bool, fetch: Bool)
+    case bolus
     case manualTempBasal
     case autotuneConfig
     case dataTable
@@ -66,12 +66,8 @@ extension Screen {
             PreferencesEditor.RootView(resolver: resolver)
         case .addTempTarget:
             AddTempTarget.RootView(resolver: resolver)
-        case let .bolus(waitForSuggestion, fetch):
-            Bolus.RootView(
-                resolver: resolver,
-                waitForSuggestion: waitForSuggestion,
-                fetch: fetch
-            )
+        case .bolus:
+            Bolus.RootView(resolver: resolver)
         case .manualTempBasal:
             ManualTempBasal.RootView(resolver: resolver)
         case .autotuneConfig: