فهرست منبع

picker for TempTarget value

* bug: misses every 10th 0.1mmol/L step
Robert 1 سال پیش
والد
کامیت
bf2268324a

+ 2 - 2
FreeAPS/Sources/Modules/OverrideConfig/OverrideStateModel.swift

@@ -108,12 +108,12 @@ extension OverrideConfig {
             guard target != 0 else { return false }
 
             if units == .mgdL,
-               target < 70 || target > 270
+               target < 80 || target > 270 // in oref min lowTT = 80!
             {
                 showInvalidTargetAlert = true
                 return true
             } else if units == .mmolL,
-                      target < 4 || target > 15
+                      target < 4.4 || target > 15 // in oref min lowTT = 80!
             {
                 showInvalidTargetAlert = true
                 return true

+ 146 - 49
FreeAPS/Sources/Modules/OverrideConfig/View/AddTempTargetForm.swift

@@ -6,6 +6,8 @@ struct AddTempTargetForm: View {
     @Environment(\.presentationMode) var presentationMode
     @Environment(\.colorScheme) var colorScheme
     @Environment(\.dismiss) var dismiss
+    @State private var targetStep: Int = 5
+    @State private var displayPickerTarget: Bool = false
     @State private var showAlert = false
     @State private var showPresetAlert = false
     @State private var alertString = ""
@@ -81,33 +83,85 @@ struct AddTempTargetForm: View {
     }
 
     @ViewBuilder private func addTempTarget() -> some View {
+        let pad: CGFloat = 3
+        VStack {
+            HStack {
+                Text("Name")
+                Spacer()
+                TextField("(Optional)", text: $state.overrideName).multilineTextAlignment(.trailing)
+            }
+            .padding(.vertical, pad)
+        }
         Section(
             header: Text("Configure Temp Target"),
             content: {
                 HStack {
                     Text("Name")
                     Spacer()
-                    TextField("Enter Name", text: $state.tempTargetName)
+                    TextField("Enter Name (optional)", text: $state.tempTargetName)
                         .multilineTextAlignment(.trailing)
                 }
-
-                HStack {
-                    Text("Target")
-                    Spacer()
-                    TextFieldWithToolBar(text: $state.tempTargetTarget, placeholder: "0", numberFormatter: glucoseFormatter)
-                        .onChange(of: state.tempTargetTarget) { _ in
-                            state.percentage = Double(state.computeAdjustedPercentage() * 100)
-                        }
-                    Text(state.units.rawValue).foregroundColor(.secondary)
-                }
-
                 HStack {
                     Text("Duration")
                     Spacer()
                     TextFieldWithToolBar(text: $state.tempTargetDuration, placeholder: "0", numberFormatter: formatter)
                     Text("minutes").foregroundColor(.secondary)
                 }
-                DatePicker("Date", selection: $state.date)
+                VStack {
+                    HStack {
+                        Text("Target Glucose")
+                        Spacer()
+                        Text(formattedGlucose(glucose: state.tempTargetTarget))
+                            .foregroundColor(!displayPickerTarget ? .primary : .accentColor)
+                    }
+                    .padding(.vertical, pad)
+                    .onTapGesture {
+                        displayPickerTarget.toggle()
+                    }
+
+                    if displayPickerTarget {
+                        HStack {
+                            // Radio buttons and text on the left side
+                            let factor = state.units == .mgdL ? 1 : 2
+                            VStack(alignment: .leading) {
+                                // Radio buttons for step iteration
+                                ForEach([1, 5], id: \.self) { step in
+                                    RadioButton(
+                                        isSelected: targetStep == step * factor,
+                                        label: "\(formattedGlucose(glucose: Decimal(step)))"
+                                    ) {
+                                        targetStep = step * factor
+                                        roundTargetToStep()
+                                    }
+                                    .padding(.top, 10)
+                                }
+                            }
+                            .frame(maxWidth: .infinity)
+
+                            Spacer()
+
+                            // Picker on the right side
+                            Picker(
+                                selection: Binding(
+                                    get: { Int(truncating: state.tempTargetTarget as NSNumber) },
+                                    set: { state.tempTargetTarget = Decimal($0) }
+                                ), label: Text("")
+                            ) {
+                                ForEach(
+                                    Array(stride(from: 80, through: 270, by: targetStep)),
+                                    id: \.self
+                                ) { glucose in
+                                    Text(formattedGlucose(glucose: Decimal(glucose)))
+                                        .tag(glucose)
+                                }
+                            }
+                            .pickerStyle(WheelPickerStyle())
+                            .frame(maxWidth: .infinity)
+                        }
+                        .frame(maxWidth: .infinity)
+                    }
+                    DatePicker("Date", selection: $state.date)
+                }
             }
         ).listRowBackground(Color.chart)
 
@@ -277,48 +331,41 @@ struct AddTempTargetForm: View {
     }
 
     private var saveButton: some View {
-        var (isInvalid, errorMessage) = isTempTargetInvalid()
+        let (isInvalid, errorMessage) = isTempTargetInvalid()
         let noNameSpecified = state.tempTargetName == ""
-        if errorMessage == nil && noNameSpecified {
-            errorMessage = "To save Preset assign a name!"
-        }
 
         return Group {
-            if errorMessage != nil {
-                Section {
-                    HStack {
-                        Spacer()
-                        Text(errorMessage ?? "")
-                            .textCase(nil)
-                            .font(.footnote)
-                            .lineLimit(1)
-                            .minimumScaleFactor(0.5)
-                        Spacer()
-                    }
-                }.listRowBackground(Color.tabBar)
-            }
-            Section {
-                Button(action: {
-                    Task {
-                        if noNameSpecified { state.tempTargetName = "Custom Target" }
-                        didPressSave.toggle()
-                        state.isTempTargetEnabled.toggle()
-                        await state.saveCustomTempTarget()
-                        await state.resetTempTargetState()
-                        dismiss()
-                    }
-                }, label: {
-                    Text("Enact Temp Target")
-                })
-                    .disabled(isInvalid)
-                    .frame(maxWidth: .infinity, alignment: .center)
-                    .tint(.white)
-            }
-            .listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
+            Section(
+                header:
+                HStack {
+                    Spacer()
+                    Text(errorMessage ?? "").textCase(nil)
+                        .foregroundColor(colorScheme == .dark ? .orange : .accentColor)
+                    Spacer()
+                },
+                content: {
+                    Button(action: {
+                        Task {
+                            if noNameSpecified { state.tempTargetName = "Custom Target" }
+                            didPressSave.toggle()
+                            state.isTempTargetEnabled.toggle()
+                            await state.saveCustomTempTarget()
+                            await state.resetTempTargetState()
+                            dismiss()
+                        }
+                    }, label: {
+                        Text("Enact Temp Target")
+                    })
+                        .disabled(isInvalid)
+                        .frame(maxWidth: .infinity, alignment: .center)
+                        .tint(.white)
+                }
+            ).listRowBackground(isInvalid ? Color(.systemGray4) : Color(.systemBlue))
 
             Section {
                 Button(action: {
                     Task {
+                        if noNameSpecified { state.tempTargetName = "Custom Target" }
                         didPressSave.toggle()
                         await state.saveTempTargetPreset()
                         dismiss()
@@ -327,7 +374,7 @@ struct AddTempTargetForm: View {
                     Text("Save as Preset")
 
                 })
-                    .disabled(isInvalid || noNameSpecified)
+                    .disabled(isInvalid)
                     .frame(maxWidth: .infinity, alignment: .center)
                     .tint(.white)
             }
@@ -341,4 +388,54 @@ struct AddTempTargetForm: View {
         let percentageNumber = NSNumber(value: value)
         return formatter.string(from: percentageNumber) ?? "\(value)"
     }
+
+    private func formattedGlucose(glucose: Decimal) -> String {
+        let formattedValue: String
+        if state.units == .mgdL {
+            formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
+        } else {
+            formattedValue = glucose.formattedAsMmolL
+        }
+        return "\(formattedValue) \(state.units.rawValue)"
+    }
+
+    private func roundTargetToStep() {
+        // Check if tempTargetTarget is not divisible by the selected step
+        if let tempTarget = state.tempTargetTarget as? Double,
+           tempTarget.truncatingRemainder(dividingBy: Double(targetStep)) != 0
+        {
+            let roundedValue: Double
+
+            if state.tempTargetTarget > 100 {
+                // Round down to the nearest valid step away from 100
+                let stepCount = (Double(state.tempTargetTarget) - 100) / Double(targetStep)
+                roundedValue = 100 + floor(stepCount) * Double(targetStep)
+            } else {
+                // Round up to the nearest valid step away from 100
+                let stepCount = (100 - Double(state.tempTargetTarget)) / Double(targetStep)
+                roundedValue = 100 - floor(stepCount) * Double(targetStep)
+            }
+
+            // Ensure the value stays higher than 79
+            state.tempTargetTarget = Decimal(max(80, roundedValue))
+        }
+    }
+}
+
+struct RadioButton: View {
+    var isSelected: Bool
+    var label: String
+    var action: () -> Void
+
+    var body: some View {
+        Button(action: {
+            action()
+        }) {
+            HStack {
+                Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
+                Text(label) // Add label inside the button to make it tappable
+            }
+        }
+        .buttonStyle(PlainButtonStyle())
+    }
 }