Procházet zdrojové kódy

Merge branch 'alpha' into Crowdin

Jon B.M před 2 roky
rodič
revize
dbc83b4036

+ 8 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -19,6 +19,7 @@
 		190EBCC829FF13AA00BA767D /* StatConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCC729FF13AA00BA767D /* StatConfigStateModel.swift */; };
 		190EBCCB29FF13CB00BA767D /* StatConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */; };
 		191F62682AD6B05A004D7911 /* NightscoutSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 191F62672AD6B05A004D7911 /* NightscoutSettings.swift */; };
+		19229B962AFBB84800CD91CA /* Predictions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19229B952AFBB84800CD91CA /* Predictions.swift */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1935363F28496F7D001E0B16 /* Oref2_variables.swift */; };
 		193F6CDD2A512C8F001240FD /* Loops.swift in Sources */ = {isa = PBXBuildFile; fileRef = 193F6CDC2A512C8F001240FD /* Loops.swift */; };
@@ -40,7 +41,7 @@
 		19D466A529AA2BD4004D5F33 /* FPUConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */; };
 		19D466A729AA2C22004D5F33 /* FPUConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */; };
 		19D466AA29AA3099004D5F33 /* FPUConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */; };
-		19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */; };
+		19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19D4E4EA29FC6A9F00351451 /* Charts.swift */; };
 		19DA48E829CD339B00EEA1E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */; };
 		19DA48E929CD339C00EEA1E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */; };
 		19DA48EA29CD339C00EEA1E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */; };
@@ -508,6 +509,7 @@
 		190EBCCA29FF13CB00BA767D /* StatConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatConfigRootView.swift; sourceTree = "<group>"; };
 		1918333A26ADA46800F45722 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
 		191F62672AD6B05A004D7911 /* NightscoutSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettings.swift; sourceTree = "<group>"; };
+		19229B952AFBB84800CD91CA /* Predictions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predictions.swift; sourceTree = "<group>"; };
 		1927C8E92744611700347C69 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EA2744611800347C69 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
 		1927C8EB2744611900347C69 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -568,7 +570,7 @@
 		19D466A429AA2BD4004D5F33 /* FPUConfigProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigProvider.swift; sourceTree = "<group>"; };
 		19D466A629AA2C22004D5F33 /* FPUConfigStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigStateModel.swift; sourceTree = "<group>"; };
 		19D466A929AA3099004D5F33 /* FPUConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPUConfigRootView.swift; sourceTree = "<group>"; };
-		19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TIRforChart.swift; sourceTree = "<group>"; };
+		19D4E4EA29FC6A9F00351451 /* Charts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Charts.swift; sourceTree = "<group>"; };
 		19DA487F29CD2B8400EEA1E7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		19DC677E29CA675700FD9EC4 /* OverrideProfilesDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProfilesDataFlow.swift; sourceTree = "<group>"; };
 		19DC678029CA676A00FD9EC4 /* OverrideProfilesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideProfilesProvider.swift; sourceTree = "<group>"; };
@@ -1655,7 +1657,7 @@
 				FE41E4D529463EE20047FD55 /* NightscoutPreferences.swift */,
 				191F62672AD6B05A004D7911 /* NightscoutSettings.swift */,
 				1967DFBD29D052C200759F30 /* Icons.swift */,
-				19D4E4EA29FC6A9F00351451 /* TIRforChart.swift */,
+				19D4E4EA29FC6A9F00351451 /* Charts.swift */,
 				19A910352A24D6D700C8951B /* DateFilter.swift */,
 				193F6CDC2A512C8F001240FD /* Loops.swift */,
 				CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */,
@@ -2084,6 +2086,7 @@
 				10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */,
 				BDFD165B2AE40688007F0DDA /* DefaultBolusCalcRootView.swift */,
 				BDFD16592AE40438007F0DDA /* AlternativeBolusCalcRootView.swift */,
+				19229B952AFBB84800CD91CA /* Predictions.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -2830,6 +2833,7 @@
 				1967DFBE29D052C200759F30 /* Icons.swift in Sources */,
 				38E8754F275556FA00975559 /* WatchManager.swift in Sources */,
 				A228DF96647338139F152B15 /* PreferencesEditorDataFlow.swift in Sources */,
+				19229B962AFBB84800CD91CA /* Predictions.swift in Sources */,
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
 				E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */,
@@ -2862,7 +2866,7 @@
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
 				1967DFC029D053AC00759F30 /* IconSelection.swift in Sources */,
 				BDFD165C2AE40688007F0DDA /* DefaultBolusCalcRootView.swift in Sources */,
-				19D4E4EB29FC6A9F00351451 /* TIRforChart.swift in Sources */,
+				19D4E4EB29FC6A9F00351451 /* Charts.swift in Sources */,
 				FEFFA7A22929FE49007B8193 /* UIDevice+Extensions.swift in Sources */,
 				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */,

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

@@ -121,6 +121,9 @@
 /*  */
 "Carbs required" = "Carbs required";
 
+/* Saved Food Presets */
+"Saved Food" = "Saved Food";
+
 /* */
 "Are you sure?" = "Are you sure?";
 
@@ -1401,6 +1404,15 @@ Enact a temp Basal or a temp target */
 "Eventual Glucose" = "Eventual Glucose";
 
 /* */
+"Please wait" = "Please wait";
+
+"Glucose, %$" = "Glucose, %$";
+
+
+
+
+
+/* */
 "Target Glucose" = "Target Glucose";
 
 /* */

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

@@ -0,0 +1,17 @@
+
+import Foundation
+
+struct ShapeModel: Identifiable {
+    var type: String
+    var percent: Decimal
+    var id = UUID()
+}
+
+struct ChartData: Identifiable {
+    var date: Date
+    var iob: Double
+    var zt: Double
+    var cob: Double
+    var uam: Double
+    var id = UUID()
+}

+ 0 - 8
FreeAPS/Sources/Models/TIRforChart.swift

@@ -1,8 +0,0 @@
-
-import Foundation
-
-struct ShapeModel: Identifiable {
-    var type: String
-    var percent: Decimal
-    var id = UUID()
-}

+ 17 - 15
FreeAPS/Sources/Modules/AddCarbs/AddCarbsStateModel.swift

@@ -140,16 +140,16 @@ extension AddCarbs {
             var addedString = ""
 
             if extracarbs > 0, filteredArray.isNotEmpty {
-                addedString += "Additional carbs: \(extracarbs) "
+                addedString += "Additional carbs: \(extracarbs) ,"
             } else if extracarbs < 0 { addedString += "Removed carbs: \(extracarbs) " }
 
             if extraFat > 0, filteredArray.isNotEmpty {
-                addedString += "Additional fat: \(extraFat) "
-            } else if extraFat < 0 { addedString += "Removed fat: \(extraFat) " }
+                addedString += "Additional fat: \(extraFat) ,"
+            } else if extraFat < 0 { addedString += "Removed fat: \(extraFat) ," }
 
             if extraProtein > 0, filteredArray.isNotEmpty {
-                addedString += "Additional protein: \(extraProtein) "
-            } else if extraProtein < 0 { addedString += "Removed protein: \(extraProtein) " }
+                addedString += "Additional protein: \(extraProtein) ,"
+            } else if extraProtein < 0 { addedString += "Removed protein: \(extraProtein) ," }
 
             if addedString != "" {
                 waitersNotepad.append(addedString)
@@ -170,7 +170,7 @@ extension AddCarbs {
 
         func loadEntries(_ editMode: Bool) {
             if editMode {
-                coredataContext.perform {
+                coredataContext.performAndWait {
                     var mealToEdit = [Meals]()
                     let requestMeal = Meals.fetchRequest() as NSFetchRequest<Meals>
                     let sortMeal = NSSortDescriptor(key: "createdAt", ascending: false)
@@ -188,15 +188,17 @@ extension AddCarbs {
         }
 
         func saveToCoreData(_ stored: [CarbsEntry]) {
-            let save = Meals(context: coredataContext)
-            save.createdAt = stored.first?.createdAt ?? .distantPast
-            save.id = stored.first?.collectionID ?? ""
-            save.carbs = Double(stored.first?.carbs ?? 0)
-            save.fat = Double(stored.first?.fat ?? 0)
-            save.protein = Double(stored.first?.protein ?? 0)
-            save.note = stored.first?.note ?? ""
-            if coredataContext.hasChanges {
-                try? coredataContext.save()
+            coredataContext.performAndWait {
+                let save = Meals(context: coredataContext)
+                if let entry = stored.first {
+                    save.createdAt = Date.now
+                    save.id = entry.collectionID ?? ""
+                    save.carbs = Double(entry.carbs)
+                    save.fat = Double(entry.fat ?? 0)
+                    save.protein = Double(entry.protein ?? 0)
+                    save.note = entry.note
+                    try? coredataContext.save()
+                }
             }
         }
     }

+ 163 - 98
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -10,6 +10,7 @@ extension AddCarbs {
         @State var dish: String = ""
         @State var isPromptPresented = false
         @State var saved = false
+        @State var pushed = false
         @State private var showAlert = false
         @FocusState private var isFocused: Bool
 
@@ -29,12 +30,12 @@ extension AddCarbs {
 
         var body: some View {
             Form {
-                if let carbsReq = state.carbsRequired {
+                if let carbsReq = state.carbsRequired, state.carbs < carbsReq {
                     Section {
                         HStack {
                             Text("Carbs required")
                             Spacer()
-                            Text(formatter.string(from: carbsReq as NSNumber)! + " g")
+                            Text((formatter.string(from: carbsReq as NSNumber) ?? "") + " g")
                         }
                     }
                 }
@@ -50,11 +51,55 @@ extension AddCarbs {
                             cleanInput: true
                         )
                         Text("grams").foregroundColor(.secondary)
-                    }.padding(.vertical)
+                    }
 
                     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 {
+                        let now = Date.now
+                        Text("Time")
+                        Spacer()
+                        if !pushed {
+                            Button {
+                                pushed = true
+                            } label: { Text("Now") }.buttonStyle(.borderless).foregroundColor(.secondary).padding(.trailing, 5)
+                        } else {
+                            Button { state.date = state.date.addingTimeInterval(-10.minutes.timeInterval) }
+                            label: { Image(systemName: "minus.circle") }.tint(.blue).buttonStyle(.borderless)
+                            DatePicker(
+                                "Time",
+                                selection: $state.date,
+                                in: ...now,
+                                displayedComponents: [.hourAndMinute]
+                            ).controlSize(.mini)
+                                .labelsHidden()
+                            Button {
+                                if state.date.addingTimeInterval(5.minutes.timeInterval) < now {
+                                    state.date = state.date.addingTimeInterval(10.minutes.timeInterval)
+                                }
+                            }
+                            label: { Image(systemName: "plus.circle") }.tint(.blue).buttonStyle(.borderless)
+                        }
+                    }
+
+                    // Optional meal note
                     HStack {
                         Text("Note").foregroundColor(.secondary)
                         TextField("", text: $state.note).multilineTextAlignment(.trailing)
@@ -62,14 +107,11 @@ extension AddCarbs {
                             Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
                                 .controlSize(.mini)
                         }
-                    }.focused($isFocused)
-                        .popover(isPresented: $isPromptPresented) {
-                            presetPopover
-                        }
-                }
-
-                Section {
-                    mealPresets
+                    }
+                    .focused($isFocused)
+                    .popover(isPresented: $isPromptPresented) {
+                        presetPopover
+                    }
                 }
 
                 Section {
@@ -77,10 +119,11 @@ extension AddCarbs {
                     label: { Text(state.skipBolus ? "Save" : "Continue") }
                         .disabled(state.carbs <= 0 && state.fat <= 0 && state.protein <= 0)
                         .frame(maxWidth: .infinity, alignment: .center)
-                } footer: { Text(state.waitersNotepad().description) }
+                }.listRowBackground(!empty ? Color(.systemBlue) : Color(.systemGray4))
+                    .tint(.white)
 
                 Section {
-                    DatePicker("Date", selection: $state.date)
+                    mealPresets
                 }
             }
             .onAppear {
@@ -93,7 +136,7 @@ extension AddCarbs {
             .navigationBarItems(leading: Button("Close", action: state.hideModal))
         }
 
-        var presetPopover: some View {
+        private var presetPopover: some View {
             Form {
                 Section {
                     TextField("Name Of Dish", text: $dish)
@@ -121,114 +164,125 @@ extension AddCarbs {
             }
         }
 
-        var notEmpty: Bool {
-            state.carbs > 0 || state.protein > 0 || state.fat > 0
+        private var empty: Bool {
+            state.carbs <= 0 && state.fat <= 0 && state.protein <= 0
+        }
+
+        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") }
+                .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") }
+                .disabled(state.selection == nil)
+                .buttonStyle(.borderless)
+                .tint(.blue)
         }
 
-        var mealPresets: some View {
+        private var mealPresets: some View {
             Section {
                 HStack {
-                    Button {
-                        isPromptPresented = true
+                    if state.selection != nil {
+                        minusButton
                     }
-                    label: { Text("Save as Preset") }
-                        .buttonStyle(BorderlessButtonStyle())
-                        .disabled(
-                            !notEmpty ||
-                                (
-                                    (((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
-                                )
-                        )
-
-                    Picker("Select a Preset", selection: $state.selection) {
-                        Text("Presets").tag(nil as Presets?)
+                    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: .trailing)
+                    .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
+                    }
                 }
 
-                if state.selection != nil {
-                    HStack {
-                        Button("Delete Preset") {
-                            showAlert.toggle()
-                        }
-                        .disabled(state.selection == nil)
-                        .tint(.orange)
-                        .buttonStyle(BorderlessButtonStyle())
-                        .alert(
-                            "Delete preset '\(state.selection?.dish ?? "")'?",
-                            isPresented: $showAlert,
-                            actions: {
-                                Button("No", role: .cancel) {}
-                                Button("Yes", role: .destructive) {
-                                    state.deletePreset()
+                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.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()
-                                }
+                                state.addPresetToNewMeal()
                             }
-                        )
-                        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: { Text("[ -1 ]") }
-                            .disabled(
-                                state
-                                    .selection == nil ||
-                                    (
-                                        !state.summation
-                                            .contains(state.selection?.dish ?? "") && (state.selection?.dish ?? "") != ""
-                                    )
-                            )
-                            .buttonStyle(BorderlessButtonStyle())
-                            .frame(maxWidth: .infinity, alignment: .trailing)
-                            .tint(.minus)
-                        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: { Text("[ +1 ]") }
-                            .disabled(state.selection == nil)
-                            .buttonStyle(BorderlessButtonStyle())
-                            .tint(.blue)
+                    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
+                                )
+                        )
                 }
             }
         }
@@ -262,3 +316,14 @@ extension AddCarbs {
         }
     }
 }
+
+public extension Color {
+    static func randomGreen(randomOpacity: Bool = false) -> Color {
+        Color(
+            red: .random(in: 0 ... 1),
+            green: .random(in: 0.4 ... 0.7),
+            blue: .random(in: 0.2 ... 1),
+            opacity: randomOpacity ? .random(in: 0.8 ... 1) : 1
+        )
+    }
+}

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

@@ -1,5 +1,3 @@
-
-import CoreData
 import LoopKit
 import SwiftUI
 import Swinject
@@ -14,9 +12,8 @@ extension Bolus {
         @Injected() var settings: SettingsManager!
         @Injected() var nsManager: NightscoutManager!
 
-        let coredataContext = CoreDataStack.shared.persistentContainer.viewContext
-
         @Published var suggestion: Suggestion?
+        @Published var predictions: Predictions?
         @Published var amount: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
         @Published var insulinRequired: Decimal = 0
@@ -60,7 +57,6 @@ extension Bolus {
         @Published var fattyMeals: Bool = false
         @Published var fattyMealFactor: Decimal = 0
         @Published var useFattyMealCorrectionFactor: Bool = false
-        @Published var eventualBG: Int = 0
 
         @Published var meal: [CarbsEntry]?
         @Published var carbs: Decimal = 0
@@ -93,6 +89,12 @@ extension Bolus {
                         }
                     }.store(in: &lifetime)
             }
+            if let notNilSugguestion = provider.suggestion {
+                suggestion = notNilSugguestion
+                if let notNilPredictions = suggestion?.predictions {
+                    predictions = notNilPredictions
+                }
+            }
         }
 
         func getDeltaBG() {

+ 42 - 19
FreeAPS/Sources/Modules/Bolus/View/AlternativeBolusCalcRootView.swift

@@ -1,3 +1,4 @@
+import Charts
 import CoreData
 import SwiftUI
 import Swinject
@@ -56,6 +57,15 @@ extension Bolus {
 
         var body: some View {
             Form {
+                Section {
+                    if state.waitForSuggestion {
+                        Text("Please wait")
+                    } else {
+                        predictionChart
+                    }
+                } header: { Text("Predictions") }
+
+                Section {}
                 if fetch {
                     Section {
                         mealEntries
@@ -63,19 +73,6 @@ extension Bolus {
                 }
 
                 Section {
-                    Button {
-                        let id_ = meal.first?.id ?? ""
-                        if fetch {
-                            keepForNextWiew = true
-                            state.backToCarbsView(complexEntry: fetch, id_)
-                        } else {
-                            state.showModal(for: .addCarbs(editMode: false))
-                        }
-                    }
-                    label: { Text(fetch ? "Edit Meal" : "Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
-                } header: { Text(!fetch ? "Meal Summary" : "") }
-
-                Section {
                     HStack {
                         Button(action: {
                             showInfo.toggle()
@@ -142,7 +139,7 @@ extension Bolus {
                             }
                         }
                     }
-                } header: { Text("Bolus Summary") }
+                } header: { Text("Bolus") }
 
                 if state.amount > 0 {
                     Section {
@@ -152,10 +149,9 @@ extension Bolus {
                         }
                         label: { Text(exceededMaxBolus ? "Max Bolus exceeded!" : "Enact bolus") }
                             .frame(maxWidth: .infinity, alignment: .center)
-                            .foregroundColor(exceededMaxBolus ? .loopRed : .accentColor)
-                            .disabled(
-                                state.amount <= 0 || state.amount > state.maxBolus
-                            )
+                            .disabled(disabled)
+                            .listRowBackground(!disabled ? Color(.systemBlue) : Color(.systemGray4))
+                            .tint(.white)
                     }
                 }
                 if state.amount <= 0 {
@@ -172,7 +168,12 @@ extension Bolus {
             .navigationTitle("Enact Bolus")
             .navigationBarTitleDisplayMode(.inline)
             .navigationBarItems(
-                leading: Button { state.hideModal() }
+                leading: Button {
+                    carbssView()
+                }
+                label: { Text(fetch ? "Back" : "Meal") },
+
+                trailing: Button { state.hideModal() }
                 label: { Text("Close") }
             )
             .onAppear {
@@ -194,6 +195,14 @@ extension Bolus {
             }
         }
 
+        var predictionChart: some View {
+            ZStack {
+                PredictionView(
+                    predictions: $state.predictions, units: $state.units, eventualBG: $state.evBG, target: $state.target
+                )
+            }
+        }
+
         // Pop-up
         var bolusInfoAlternativeCalculator: some View {
             VStack {
@@ -257,6 +266,10 @@ extension Bolus {
             )
         }
 
+        private 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)
         }
@@ -265,6 +278,16 @@ extension Bolus {
             ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
         }
 
+        func carbssView() {
+            let id_ = meal.first?.id ?? ""
+            if fetch {
+                keepForNextWiew = true
+                state.backToCarbsView(complexEntry: fetch, id_)
+            } else {
+                state.showModal(for: .addCarbs(editMode: false))
+            }
+        }
+
         var mealEntries: some View {
             VStack {
                 if let carbs = meal.first?.carbs, carbs > 0 {

+ 89 - 59
FreeAPS/Sources/Modules/Bolus/View/DefaultBolusCalcRootView.swift

@@ -1,3 +1,5 @@
+import Charts
+import CoreData
 import SwiftUI
 import Swinject
 
@@ -35,58 +37,21 @@ extension Bolus {
 
         var body: some View {
             Form {
+                Section {
+                    if state.waitForSuggestion {
+                        Text("Please wait")
+                    } else {
+                        predictionChart
+                    }
+                } header: { Text("Predictions") }
+
                 if fetch {
                     Section {
-                        VStack {
-                            if let carbs = meal.first?.carbs, carbs > 0 {
-                                HStack {
-                                    Text("Carbs")
-                                    Spacer()
-                                    Text(carbs.formatted())
-                                    Text("g")
-                                }.foregroundColor(.secondary)
-                            }
-                            if let fat = meal.first?.fat, fat > 0 {
-                                HStack {
-                                    Text("Fat")
-                                    Spacer()
-                                    Text(fat.formatted())
-                                    Text("g")
-                                }.foregroundColor(.secondary)
-                            }
-                            if let protein = meal.first?.protein, protein > 0 {
-                                HStack {
-                                    Text("Protein")
-                                    Spacer()
-                                    Text(protein.formatted())
-                                    Text("g")
-                                }.foregroundColor(.secondary)
-                            }
-                            if let note = meal.first?.note, note != "" {
-                                HStack {
-                                    Text("Note")
-                                    Spacer()
-                                    Text(note)
-                                }.foregroundColor(.secondary)
-                            }
-                        }
+                        mealEntries
                     } header: { Text("Meal Summary") }
                 }
 
                 Section {
-                    Button {
-                        let id_ = meal.first?.id ?? ""
-                        if fetch {
-                            keepForNextWiew = true
-                            state.backToCarbsView(complexEntry: fetch, id_)
-                        } else {
-                            state.showModal(for: .addCarbs(editMode: false))
-                        }
-                    }
-                    label: { Text(fetch ? "Edit Meal" : "Add Meal") }.frame(maxWidth: .infinity, alignment: .center)
-                } header: { Text(!fetch ? "Meal Summary" : "") }
-
-                Section {
                     if state.waitForSuggestion {
                         HStack {
                             Text("Wait please").foregroundColor(.secondary)
@@ -129,8 +94,7 @@ extension Bolus {
                             Text(!(state.amount > state.maxBolus) ? "U" : "😵").foregroundColor(.secondary)
                         }
                     }
-                }
-                header: { Text("Bolus Summary") }
+                } header: { Text("Bolus") }
 
                 if !state.waitForSuggestion {
                     if state.amount > 0 {
@@ -141,19 +105,19 @@ extension Bolus {
                             }
                             label: { Text(!(state.amount > state.maxBolus) ? "Enact bolus" : "Max Bolus exceeded!") }
                                 .frame(maxWidth: .infinity, alignment: .center)
-                                .disabled(
-                                    state.amount <= 0 || state.amount > state.maxBolus
-                                )
+                                .disabled(disabled)
+                                .listRowBackground(!disabled ? Color(.systemBlue) : Color(.systemGray4))
+                                .tint(.white)
                         }
                     }
-                    if state.amount <= 0 {
-                        Section {
-                            Button {
-                                keepForNextWiew = true
-                                state.showModal(for: nil)
-                            }
-                            label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
+                }
+                if state.amount <= 0 {
+                    Section {
+                        Button {
+                            keepForNextWiew = true
+                            state.showModal(for: nil)
                         }
+                        label: { Text("Continue without bolus") }.frame(maxWidth: .infinity, alignment: .center)
                     }
                 }
             }
@@ -187,12 +151,32 @@ extension Bolus {
 
             .navigationTitle("Enact Bolus")
             .navigationBarTitleDisplayMode(.inline)
-            .navigationBarItems(leading: Button("Close", action: state.hideModal))
+            .navigationBarItems(
+                leading: Button {
+                    carbssView()
+                }
+                label: { Text(fetch ? "Back" : "Meal") },
+
+                trailing: 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 predictionChart: some View {
+            ZStack {
+                PredictionView(
+                    predictions: $state.predictions, units: $state.units, eventualBG: $state.evBG, target: $state.target
+                )
+            }
+        }
+
         var changed: Bool {
             ((meal.first?.carbs ?? 0) > 0) || ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
         }
@@ -201,6 +185,52 @@ extension Bolus {
             ((meal.first?.fat ?? 0) > 0) || ((meal.first?.protein ?? 0) > 0)
         }
 
+        func carbssView() {
+            let id_ = meal.first?.id ?? ""
+            if fetch {
+                keepForNextWiew = true
+                state.backToCarbsView(complexEntry: fetch, id_)
+            } else {
+                state.showModal(for: .addCarbs(editMode: false))
+            }
+        }
+
+        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

+ 110 - 0
FreeAPS/Sources/Modules/Bolus/View/Predictions.swift

@@ -0,0 +1,110 @@
+import Charts
+import CoreData
+import SwiftUI
+import Swinject
+
+struct PredictionView: View {
+    @Binding var predictions: Predictions?
+    @Binding var units: GlucoseUnits
+    @Binding var eventualBG: Int
+    @Binding var target: Decimal
+
+    private enum Config {
+        static let height: CGFloat = 160
+        static let lineWidth: CGFloat = 2
+    }
+
+    var body: some View {
+        VStack {
+            chart()
+            HStack {
+                let conversion = units == .mmolL ? 0.0555 : 1
+                Text("Eventual Glucose")
+                Spacer()
+                Text(
+                    (Double(eventualBG) * conversion)
+                        .formatted(.number.grouping(.never).rounded().precision(.fractionLength(units == .mmolL ? 1 : 0)))
+                )
+                Text(units.rawValue).foregroundStyle(.secondary)
+                Divider()
+            }.font(.callout)
+        }
+    }
+
+    func chart() -> some View {
+        // Data Source
+        let iob = predictions?.iob ?? [Int]()
+        let cob = predictions?.cob ?? [Int]()
+        let uam = predictions?.uam ?? [Int]()
+        let zt = predictions?.zt ?? [Int]()
+        let count = max(iob.count, cob.count, uam.count, zt.count)
+        var now = Date.now
+        var startIndex = 0
+        let conversion = units == .mmolL ? 0.0555 : 1
+        // Organize the data needed for prediction chart.
+        var data = [ChartData]()
+        repeat {
+            now = now.addingTimeInterval(5.minutes.timeInterval)
+            if startIndex < count {
+                let addedData = ChartData(
+                    date: now,
+                    iob: startIndex < iob.count ? Double(iob[startIndex]) * conversion : 0,
+                    zt: startIndex < zt.count ? Double(zt[startIndex]) * conversion : 0,
+                    cob: startIndex < cob.count ? Double(cob[startIndex]) * conversion : 0,
+                    uam: startIndex < uam.count ? Double(uam[startIndex]) * conversion : 0,
+                    id: UUID()
+                )
+                data.append(addedData)
+            }
+            startIndex += 1
+        } while startIndex < count
+        // Chart
+        return Chart(data) {
+            // Remove 0 (empty) values
+            if $0.iob != 0 {
+                LineMark(
+                    x: .value("Time", $0.date),
+                    y: .value("IOB", $0.iob),
+                    series: .value("IOB", "A")
+                )
+                .foregroundStyle(Color(.insulin))
+                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
+            }
+            if $0.uam != 0 {
+                LineMark(
+                    x: .value("Time", $0.date),
+                    y: .value("UAM", $0.uam),
+                    series: .value("UAM", "B")
+                )
+                .foregroundStyle(Color(.UAM))
+                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
+            }
+            if $0.cob != 0 {
+                LineMark(
+                    x: .value("Time", $0.date),
+                    y: .value("COB", $0.cob),
+                    series: .value("COB", "C")
+                )
+                .foregroundStyle(Color(.loopYellow))
+                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
+            }
+            if $0.zt != 0 {
+                LineMark(
+                    x: .value("Time", $0.date),
+                    y: .value("ZT", $0.zt),
+                    series: .value("ZT", "D")
+                )
+                .foregroundStyle(Color(.ZT))
+                .lineStyle(StrokeStyle(lineWidth: Config.lineWidth))
+            }
+        }
+        .frame(minHeight: Config.height)
+        .chartForegroundStyleScale([
+            "IOB": Color(.insulin),
+            "UAM": Color(.UAM),
+            "COB": Color(.loopYellow),
+            "ZT": Color(.ZT)
+        ])
+        .chartYAxisLabel("Glucose, " + units.rawValue, alignment: .center)
+    }
+}

+ 1 - 0
FreeAPS/Sources/Modules/StatConfig/StatConfigStateModel.swift

@@ -24,6 +24,7 @@ extension StatConfig {
             subscribeSetting(\.rulerMarks, on: $rulerMarks) { rulerMarks = $0 }
             subscribeSetting(\.useFPUconversion, on: $useFPUconversion) { useFPUconversion = $0 }
             subscribeSetting(\.skipBolusScreenAfterCarbs, on: $skipBolusScreenAfterCarbs) { skipBolusScreenAfterCarbs = $0 }
+            subscribeSetting(\.oneDimensionalGraph, on: $oneDimensionalGraph) { oneDimensionalGraph = $0 }
 
             subscribeSetting(\.low, on: $low, initial: {
                 let value = max(min($0, 90), 40)

+ 1 - 1
FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -62,7 +62,7 @@ extension StatConfig {
                 } header: { Text("Add Meal View settings ") }
             }
             .onAppear(perform: configureView)
-            .navigationBarTitle("UI/UX Settings")
+            .navigationBarTitle("UI/UX")
             .navigationBarTitleDisplayMode(.automatic)
         }
     }