Просмотр исходного кода

Refactor therapy settings input; start with BG targets WIP

Deniz Cengiz 1 год назад
Родитель
Сommit
7e5de2138b

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -591,6 +591,7 @@
 		DDAA29852D2D1D9E006546A1 /* AdjustmentsRootView+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAA29842D2D1D98006546A1 /* AdjustmentsRootView+TempTargets.swift */; };
 		DDB37CC52D05048F00D99BF4 /* ContactImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */; };
 		DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB37CC62D05127500D99BF4 /* FontExtensions.swift */; };
+		DDC38E102D9B377800ADCB46 /* OnboardingView+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */; };
 		DDCAE8332D78D4A800B1BB51 /* TherapySettingsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */; };
 		DDCE790F2D6F97FC000A4D7A /* SubmodulesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */; };
 		DDCEBF5B2CC1B76400DF4C36 /* LiveActivity+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */; };
@@ -1367,6 +1368,7 @@
 		DDB37CC32D05044D00D99BF4 /* ContactTrickEntryStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContactTrickEntryStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		DDB37CC42D05048F00D99BF4 /* ContactImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageStorage.swift; sourceTree = "<group>"; };
 		DDB37CC62D05127500D99BF4 /* FontExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontExtensions.swift; sourceTree = "<group>"; };
+		DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OnboardingView+Util.swift"; sourceTree = "<group>"; };
 		DDCAE8322D78D49C00B1BB51 /* TherapySettingsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TherapySettingsUtil.swift; sourceTree = "<group>"; };
 		DDCE790E2D6F97F7000A4D7A /* SubmodulesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubmodulesView.swift; sourceTree = "<group>"; };
 		DDCEBF5A2CC1B76400DF4C36 /* LiveActivity+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LiveActivity+Helper.swift"; sourceTree = "<group>"; };
@@ -2721,6 +2723,7 @@
 		BD47FD152D88AAD80043966B /* View */ = {
 			isa = PBXGroup;
 			children = (
+				DDC38E0F2D9B376900ADCB46 /* OnboardingView+Util.swift */,
 				BD47FD182D88AAF90043966B /* OnboardingView.swift */,
 				BD47FDD52D8B64AE0043966B /* OnboardingSteps */,
 			);
@@ -4200,6 +4203,7 @@
 				3B2F77882D7E5387005ED9FA /* CurrentTDDSetup.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				BDF34EBE2C0A31D100D51995 /* CustomNotification.swift in Sources */,
+				DDC38E102D9B377800ADCB46 /* OnboardingView+Util.swift in Sources */,
 				BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
 				DDB37CC72D05127500D99BF4 /* FontExtensions.swift in Sources */,

+ 11 - 15
Trio/Sources/Application/TrioApp.swift

@@ -138,8 +138,6 @@ extension Notification.Name {
                     cleanupOldData()
 
                     self.initState.complete = true
-                    debug(.default, "showonbaording: \(onboardingManager.shouldShowOnboarding)")
-                    self.showOnboardingView = onboardingManager.shouldShowOnboarding
                     Foundation.NotificationCenter.default.post(name: .initializationCompleted, object: nil)
                     UIApplication.shared.registerForRemoteNotifications()
                     do {
@@ -196,20 +194,18 @@ extension Notification.Name {
                     .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
                         self.showLoadingError = true
                     }
+            } else if onboardingManager.shouldShowOnboarding {
+                // Show onboarding if needed
+                Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
+                    .preferredColorScheme(colorScheme(for: .dark) ?? nil)
+                    .transition(.opacity)
             } else {
-                if showOnboardingView {
-                    // Show onboarding if needed
-                    Onboarding.RootView(resolver: resolver, onboardingManager: onboardingManager)
-                        .preferredColorScheme(colorScheme(for: .dark) ?? nil)
-                        .transition(.opacity)
-                } else {
-                    Main.RootView(resolver: resolver)
-                        .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
-                        .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
-                        .environment(appState)
-                        .environmentObject(Icons())
-                        .onOpenURL(perform: handleURL)
-                }
+                Main.RootView(resolver: resolver)
+                    .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
+                    .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
+                    .environment(appState)
+                    .environmentObject(Icons())
+                    .onOpenURL(perform: handleURL)
             }
         }
         .onChange(of: scenePhase) { _, newScenePhase in

+ 20 - 28
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -5381,6 +5381,19 @@
         }
       }
     },
+    "%.1f" : {
+
+    },
+    "%.1f %@" : {
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "new",
+            "value" : "%1$.1f %2$@"
+          }
+        }
+      }
+    },
     "%.2f U of %.2f U" : {
       "comment" : "Format for showing delivered and active bolus amounts, 'x U of y U' on watch",
       "localizations" : {
@@ -39411,9 +39424,6 @@
         }
       }
     },
-    "Blood Glucose Units" : {
-
-    },
     "Bluetooth Power Off" : {
       "comment" : "Bluetooth Power Off",
       "extractionState" : "manual",
@@ -48697,9 +48707,6 @@
     "Choose when this sensitivity factor should start" : {
 
     },
-    "Choose when this target should start" : {
-
-    },
     "Choose whether or not to display one or both X- and Y-Axis grid lines." : {
       "localizations" : {
         "bg" : {
@@ -78630,6 +78637,9 @@
         }
       }
     },
+    "Entries" : {
+
+    },
     "Error" : {
       "comment" : "Error title",
       "localizations" : {
@@ -93159,6 +93169,9 @@
         }
       }
     },
+    "Hi there!" : {
+
+    },
     "Hidden" : {
       "localizations" : {
         "bg" : {
@@ -94309,9 +94322,6 @@
         }
       }
     },
-    "High Target" : {
-
-    },
     "High Temp Target Raises Sensitivity" : {
       "comment" : "High Temp Target Raises Sensitivity",
       "localizations" : {
@@ -110762,9 +110772,6 @@
         }
       }
     },
-    "Low Target" : {
-
-    },
     "Low Temp Target Lowers Sensitivity" : {
       "comment" : "Low Temp Target Lowers Sensitivity",
       "localizations" : {
@@ -147033,9 +147040,6 @@
         }
       }
     },
-    "Select Blood Glucose Units" : {
-
-    },
     "Select CGM Model" : {
       "localizations" : {
         "bg" : {
@@ -164031,9 +164035,6 @@
     "Target glucose is out of range (%@)." : {
 
     },
-    "Target Glucose Range" : {
-
-    },
     "Target presets" : {
       "comment" : "Debug option view Target presets",
       "extractionState" : "manual",
@@ -170445,9 +170446,6 @@
         }
       }
     },
-    "These values reflect your personal target range and can be adjusted at any time in the Settings." : {
-
-    },
     "This adjusted ISF is temporary, will change with the next loop cycle, and should not be directly used as your profile ISF value." : {
       "localizations" : {
         "bg" : {
@@ -174862,9 +174860,6 @@
         }
       }
     },
-    "This range defines your ideal blood glucose values. Trio uses this to calculate insulin doses." : {
-
-    },
     "This range is for display and statistical purposes only and does not influence insulin dosing." : {
       "localizations" : {
         "bg" : {
@@ -191822,7 +191817,7 @@
         }
       }
     },
-    "Welcome to Trio!" : {
+    "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations." : {
 
     },
     "What is the numeric value of the carb to add" : {
@@ -196339,9 +196334,6 @@
         }
       }
     },
-    "Your Target Range" : {
-
-    },
     "ZT" : {
       "localizations" : {
         "bg" : {

+ 21 - 8
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -147,10 +147,6 @@ extension Onboarding {
             return settingsProvider.generatePickerValues(from: glucoseSetting, units: units)
         }
 
-        // Glucose Target
-        var targetLow: Decimal = 70
-        var targetHigh: Decimal = 180
-
         // Basal Profile
         var basalRates: [BasalRateEntry] = [BasalRateEntry(startTime: 0, rate: 1.0)]
 
@@ -184,10 +180,6 @@ extension Onboarding {
             // Make a copy of the current settings that we can mutate
             var settingsCopy = settingsManager.settings
 
-            // Apply glucose target - We'll use lowGlucose and highGlucose properties
-            settingsCopy.lowGlucose = targetLow
-            settingsCopy.highGlucose = targetHigh
-
             // Apply glucose units
             settingsCopy.units = units
 
@@ -208,6 +200,27 @@ extension Onboarding {
             // we'll directly set the settings property which will trigger the didSet observer
             settingsManager.settings = settingsCopy
         }
+
+        func getTherapyItems(from targets: [TargetsEditor.Item]) -> [TherapySettingItem] {
+            targets.map {
+                TherapySettingItem(
+                    id: UUID(),
+                    time: targetTimeValues[$0.timeIndex],
+                    value: Double(targetRateValues[$0.lowIndex])
+                )
+            }
+        }
+
+        func updateTargets(from therapyItems: [TherapySettingItem]) {
+            targetItems = therapyItems.map { item in
+                let timeIndex = targetTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
+                let closestRate = targetRateValues.enumerated().min(by: {
+                    abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
+                })?.offset ?? 0
+
+                return TargetsEditor.Item(lowIndex: closestRate, highIndex: closestRate, timeIndex: timeIndex)
+            }
+        }
     }
 }
 

+ 19 - 321
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/GlucoseTargetStepView.swift

@@ -11,12 +11,12 @@ import UIKit
 /// Glucose target step view for setting target glucose range.
 struct GlucoseTargetStepView: View {
     @Bindable var state: Onboarding.StateModel
-    @State private var showUnitPicker = false
     @State private var showTimeSelector = false
     @State private var selectedTargetIndex: Int?
     @State private var showAlert = false
     @State private var errorMessage = ""
     @State private var refreshUI = UUID() // to update chart when slider value changes
+    @State private var therapyItems: [TherapySettingItem] = []
 
     // Formatter for glucose values
     private var numberFormatter: NumberFormatter {
@@ -40,125 +40,6 @@ struct GlucoseTargetStepView: View {
     var body: some View {
         ScrollView {
             VStack(alignment: .leading, spacing: 20) {
-                // Unit selector
-                HStack {
-                    Text("Blood Glucose Units")
-                        .font(.headline)
-
-                    Spacer()
-
-                    Button(action: {
-                        showUnitPicker.toggle()
-                    }) {
-                        HStack {
-                            Text(state.units == .mgdL ? "mg/dL" : "mmol/L")
-                            Image(systemName: "chevron.down")
-                        }
-                        .padding(.horizontal, 12)
-                        .padding(.vertical, 8)
-                        .background(Color.blue.opacity(0.1))
-                        .cornerRadius(8)
-                    }
-                    .actionSheet(isPresented: $showUnitPicker) {
-                        let mgdlAction = ActionSheet.Button.default(Text("mg/dL")) {
-                            // Store current unit
-                            let oldUnit = state.units
-                            // Change to new unit
-                            state.units = .mgdL
-                            // Adjust values for unit change, only if unit actually changed
-                            if oldUnit != .mgdL {
-                                state.targetLow = max(70, state.targetLow * 18)
-                                state.targetHigh = max(120, state.targetHigh * 18)
-                                state.isf = max(30, state.isf * 18)
-                            }
-                        }
-
-                        let mmolAction = ActionSheet.Button.default(Text("mmol/L")) {
-                            // Store current unit
-                            let oldUnit = state.units
-                            // Change to new unit
-                            state.units = .mmolL
-                            // Adjust values for unit change, only if unit actually changed
-                            if oldUnit != .mmolL {
-                                state.targetLow = max(3.9, state.targetLow / 18)
-                                state.targetHigh = max(6.7, state.targetHigh / 18)
-                                state.isf = max(1.7, state.isf / 18)
-                            }
-                        }
-
-                        let cancelAction = ActionSheet.Button.cancel()
-
-                        return ActionSheet(
-                            title: Text("Select Blood Glucose Units"),
-                            buttons: [mgdlAction, mmolAction, cancelAction]
-                        )
-                    }
-                }
-
-                Divider()
-
-                // Target glucose range
-                VStack(alignment: .leading, spacing: 12) {
-                    Text("Target Glucose Range")
-                        .font(.headline)
-
-                    Text("This range defines your ideal blood glucose values. Trio uses this to calculate insulin doses.")
-                        .font(.subheadline)
-                        .foregroundColor(.secondary)
-
-                    // Low target
-                    VStack(alignment: .leading) {
-                        Text("Low Target")
-                            .font(.subheadline)
-
-                        HStack {
-                            Slider(
-                                value: Binding(
-                                    get: { Double(truncating: state.targetLow as NSNumber) },
-                                    set: { state.targetLow = Decimal($0) }
-                                ),
-                                in: state.units == .mgdL ? 70 ... 120 : 3.9 ... 6.7,
-                                step: state.units == .mgdL ? 1 : 0.1
-                            )
-                            .accentColor(.green)
-
-                            Text(
-                                "\(numberFormatter.string(from: state.targetLow as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
-                            )
-                            .frame(width: 80, alignment: .trailing)
-                        }
-                    }
-                    .padding(.vertical, 4)
-
-                    // High target
-                    VStack(alignment: .leading) {
-                        Text("High Target")
-                            .font(.subheadline)
-
-                        HStack {
-                            Slider(
-                                value: Binding(
-                                    get: { Double(truncating: state.targetHigh as NSNumber) },
-                                    set: { state.targetHigh = Decimal($0) }
-                                ),
-                                in: state.units == .mgdL ?
-                                    Double(truncating: state.targetLow as NSNumber) + 10 ... 200 :
-                                    Double(truncating: state.targetLow as NSNumber) + 0.6 ... 11.1,
-                                step: state.units == .mgdL ? 1 : 0.1
-                            )
-                            .accentColor(.green)
-
-                            Text(
-                                "\(numberFormatter.string(from: state.targetHigh as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
-                            )
-                            .frame(width: 80, alignment: .trailing)
-                        }
-                    }
-                    .padding(.vertical, 4)
-                }
-
-                Divider()
-
                 // Chart visualization
                 if !state.targetItems.isEmpty {
                     VStack(alignment: .leading) {
@@ -176,210 +57,27 @@ struct GlucoseTargetStepView: View {
                 }
 
                 // Glucose target list
-                VStack(alignment: .leading, spacing: 10) {
-                    HStack {
-                        Text("Glucose Targets")
-                            .font(.headline)
-
-                        Spacer()
-
-                        // Add new target button
-                        if state.targetItems.count < 24 {
-                            Button(action: {
-                                showTimeSelector = true
-                            }) {
-                                HStack {
-                                    Image(systemName: "plus.circle.fill")
-                                    Text("Add Target")
-                                }
-                                .foregroundColor(.blue)
-                            }
-                            .disabled(!canAddTarget)
-                        }
-                    }
-                    .padding(.horizontal)
-
-                    // List of targets
-                    VStack(spacing: 2) {
-                        ForEach(state.targetItems.indices, id: \.self) { index in
-                            let item = state.targetItems[index]
-                            HStack {
-                                // Time display
-                                Text(
-                                    dateFormatter
-                                        .string(from: Date(
-                                            timeIntervalSince1970: state
-                                                .targetTimeValues[item.timeIndex]
-                                        ))
-                                )
-                                .frame(width: 80, alignment: .leading)
-                                .padding(.leading)
-
-                                // Low target slider
-                                Slider(
-                                    value: Binding(
-                                        get: {
-                                            Double(
-                                                truncating: state
-                                                    .targetRateValues[item.lowIndex] as NSNumber
-                                            ) },
-                                        set: { newValue in
-                                            // Find closest match in rateValues array
-                                            let newIndex = state.targetRateValues
-                                                .firstIndex { abs(Double($0) - newValue) < 0.05 } ?? item.lowIndex
-                                            state.targetItems[index].lowIndex = newIndex
-
-                                            // Ensure high target is at least as high as low target
-                                            if state.targetItems[index].highIndex < newIndex {
-                                                state.targetItems[index].highIndex = newIndex
-                                            }
-
-                                            // Force refresh when slider changes
-                                            refreshUI = UUID()
-                                        }
-                                    ),
-                                    in: Double(truncating: state.targetRateValues.first! as NSNumber) ...
-                                        Double(truncating: state.targetRateValues.last! as NSNumber),
-                                    step: state.units == .mgdL ? 1 : 0.1
-                                )
-                                .accentColor(.blue)
-                                .padding(.horizontal, 5)
-                                .onChange(of: state.targetItems[index].lowIndex) { _, _ in
-                                    let impact = UIImpactFeedbackGenerator(style: .light)
-                                    impact.impactOccurred()
-                                }
-
-                                // Display the current value
-                                Text(
-                                    "\(numberFormatter.string(from: state.targetRateValues[item.lowIndex] as NSNumber) ?? "--") \(state.units == .mgdL ? "mg/dL" : "mmol/L")"
-                                )
-                                .frame(width: 80, alignment: .trailing)
-                                .lineLimit(1)
-                                .minimumScaleFactor(0.8)
-
-                                // Delete button (not for the first entry at 00:00)
-                                if index > 0 {
-                                    Button(action: {
-                                        state.targetItems.remove(at: index)
-                                    }) {
-                                        Image(systemName: "trash")
-                                            .foregroundColor(.red)
-                                            .padding(.horizontal, 5)
-                                    }
-                                } else {
-                                    // Spacer to maintain alignment
-                                    Spacer()
-                                        .frame(width: 30)
-                                }
-                            }
-                            .padding(.vertical, 12)
-                            .background(index % 2 == 0 ? Color.blue.opacity(0.05) : Color.clear)
-                            .cornerRadius(8)
-                        }
-                    }
-                    .background(Color.blue.opacity(0.05))
-                    .cornerRadius(10)
-                    .padding(.horizontal)
-                    .onAppear {
-                        if state.targetItems.isEmpty {
-                            state.addTarget()
-                        }
-                    }
-                }
-
-                // Target range visualization
-                VStack(alignment: .leading, spacing: 8) {
-                    Text("Your Target Range")
-                        .font(.headline)
-
-                    HStack(spacing: 0) {
-                        // Below range
-                        Rectangle()
-                            .fill(Color.red.opacity(0.3))
-                            .frame(width: 50, height: 30)
-                            .overlay(
-                                Text("Low")
-                                    .font(.caption)
-                                    .foregroundColor(.red)
-                            )
-
-                        // Target range
-                        Rectangle()
-                            .fill(Color.green.opacity(0.3))
-                            .frame(width: 100, height: 30)
-                            .overlay(
-                                Text("Target")
-                                    .font(.caption)
-                                    .foregroundColor(.green)
-                            )
-
-                        // Above range
-                        Rectangle()
-                            .fill(Color.yellow.opacity(0.3))
-                            .frame(width: 50, height: 30)
-                            .overlay(
-                                Text("High")
-                                    .font(.caption)
-                                    .foregroundColor(.orange)
-                            )
-                    }
-                    .cornerRadius(8)
-
-                    // Range values
-                    HStack(spacing: 0) {
-                        Text("\(numberFormatter.string(from: state.targetLow as NSNumber) ?? "--")")
-                            .font(.caption)
-                            .frame(width: 50, alignment: .center)
-
-                        Spacer()
-                            .frame(width: 100)
-
-                        Text("\(numberFormatter.string(from: state.targetHigh as NSNumber) ?? "--")")
-                            .font(.caption)
-                            .frame(width: 50, alignment: .center)
-                    }
-
-                    Text("These values reflect your personal target range and can be adjusted at any time in the Settings.")
-                        .font(.caption)
-                        .foregroundColor(.secondary)
-                        .padding(.top, 8)
+                VStack(alignment: .leading) {
+                    Text("Glucose Targets")
+                        .font(.title2)
+                        .padding(.horizontal)
+
+                    TimeValueEditorView(
+                        items: $therapyItems,
+                        unit: state.units.rawValue,
+                        valueOptions: state.targetRateValues
+                    )
                 }
             }
-            .padding()
-        }
-        .actionSheet(isPresented: $showTimeSelector) {
-            var buttons: [ActionSheet.Button] = []
-
-            // Find available time slots in 1-hour increments
-            for hour in 0 ..< 24 {
-                let hourInMinutes = hour * 60
-                // Calculate timeIndex for this hour
-                let timeIndex = state.targetTimeValues.firstIndex { abs($0 - Double(hourInMinutes * 60)) < 10 } ?? 0
-
-                // Check if this hour is already in the profile
-                if !state.targetItems.contains(where: { $0.timeIndex == timeIndex }) {
-                    buttons.append(.default(Text("\(String(format: "%02d:00", hour))")) {
-                        // Get the current low and high values from the last item
-                        let lowIndex = state.targetItems.last?.lowIndex ?? 0
-                        let highIndex = state.targetItems.last?.highIndex ?? lowIndex
-
-                        // Create new item with the specified time
-                        let newItem = TargetsEditor.Item(lowIndex: lowIndex, highIndex: highIndex, timeIndex: timeIndex)
-
-                        // Add the new item and sort the list by timeIndex
-                        state.targetItems.append(newItem)
-                        state.targetItems.sort(by: { $0.timeIndex < $1.timeIndex })
-                    })
-                }
+        }.onAppear {
+            if state.targetItems.isEmpty {
+                state.addTarget()
             }
-
-            buttons.append(.cancel())
-
-            return ActionSheet(
-                title: Text("Select Start Time"),
-                message: Text("Choose when this target should start"),
-                buttons: buttons
-            )
+            therapyItems = state.getTherapyItems(from: state.targetItems)
+            debug(.default, "THERAPY ITEMS: \(therapyItems)")
+        }.onChange(of: therapyItems) { _, newItems in
+            state.updateTargets(from: newItems)
+            refreshUI = UUID()
         }
     }
 

+ 15 - 8
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OnboardingStepViews.swift

@@ -4,12 +4,24 @@ import SwiftUI
 struct WelcomeStepView: View {
     var body: some View {
         VStack(alignment: .center, spacing: 20) {
-            Text("Welcome to Trio!")
+            Image("trioCircledNoBackground")
+                .resizable()
+                .scaledToFit()
+                .frame(height: 100)
+                .padding()
+
+            Text("Hi there!")
                 .font(.title2)
                 .fontWeight(.bold)
                 .multilineTextAlignment(.center)
 
             Text(
+                "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
+            )
+            .multilineTextAlignment(.center)
+            .foregroundColor(.secondary)
+
+            Text(
                 "Trio is designed to help manage your diabetes efficiently. To get the most out of the app, we'll guide you through setting up some essential parameters."
             )
             .multilineTextAlignment(.center)
@@ -17,13 +29,8 @@ struct WelcomeStepView: View {
 
             Text("Let's go through a few quick steps to ensure Trio works optimally for you.")
                 .multilineTextAlignment(.center)
-                .foregroundColor(.secondary)
-
-            Image("trioCircledNoBackground")
-                .resizable()
-                .scaledToFit()
-                .frame(height: 100)
-                .padding()
+                .foregroundColor(.primary)
+                .bold()
         }
         .padding()
         .frame(maxWidth: .infinity)

+ 141 - 0
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -0,0 +1,141 @@
+import SwiftUI
+
+struct TherapySettingItem: Identifiable, Equatable {
+    var id = UUID()
+    var time: TimeInterval // seconds since start of day
+    var value: Double
+}
+
+struct TimeValuePickerRow: View {
+    @Binding var item: TherapySettingItem
+    var valueOptions: [Decimal]
+    var unit: String
+
+    var body: some View {
+        VStack(spacing: 8) {
+            HStack {
+                Picker("Time", selection: Binding(
+                    get: { item.time },
+                    set: { item.time = $0 }
+                )) {
+                    ForEach(0 ..< 48) { i in
+                        let seconds = Double(i * 30 * 60)
+                        Text(timeFormatter.string(from: Date(timeIntervalSince1970: seconds)))
+                            .tag(seconds)
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .clipped()
+
+                Picker("Value", selection: Binding(
+                    get: { item.value },
+                    set: { item.value = $0 }
+                )) {
+                    ForEach(valueOptions, id: \.self) { value in
+                        Text("\(Double(value), specifier: "%.1f") \(unit)").tag(Double(value))
+                    }
+                }
+                .frame(maxWidth: .infinity)
+                .clipped()
+            }
+            .pickerStyle(.wheel)
+        }
+        .padding(.vertical, 8)
+    }
+
+    private var timeFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH:mm"
+        return formatter
+    }
+}
+
+struct TimeValueEditorView: View {
+    @Binding var items: [TherapySettingItem]
+    var unit: String
+    var valueOptions: [Decimal]
+
+    @State private var selectedItemID: UUID?
+
+    var body: some View {
+        List {
+            HStack {
+                Text("Entries").bold()
+                Spacer()
+                Button {
+                    let lastTime = items.last?.time ?? 0
+                    let newTime = min(lastTime + 1800, 23 * 3600 + 1800)
+                    let newValue = items.last?.value ?? 1.0
+                    items.append(TherapySettingItem(time: newTime, value: newValue))
+                } label: {
+                    HStack {
+                        Image(systemName: "plus")
+                        Text("Add")
+                            .foregroundColor(.accentColor)
+                    }
+                }
+                .disabled(items.count >= 48)
+            }
+
+            ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
+                VStack(spacing: 0) {
+                    Button {
+                        selectedItemID = selectedItemID == item.id ? nil : item.id
+                    } label: {
+                        HStack {
+                            Text(timeFormatter.string(from: Date(timeIntervalSince1970: item.time)))
+                                .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
+                            Spacer()
+                            HStack {
+                                Text("\(item.value, specifier: "%.1f")")
+                                    .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
+                                Text(unit.description)
+                                    .foregroundStyle(Color.secondary)
+                            }
+                        }.contentShape(Rectangle())
+                    }
+                    .buttonStyle(.plain)
+                    .padding(.vertical, 8)
+
+                    if selectedItemID == item.id {
+                        TimeValuePickerRow(
+                            item: $items[index],
+                            valueOptions: valueOptions,
+                            unit: unit
+                        )
+                        .transition(.slide)
+                    }
+                }
+                .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+                    if index > 0 {
+                        Button(role: .destructive) {
+                            items.remove(at: index)
+                            selectedItemID = nil
+                        } label: {
+                            Label("Delete", systemImage: "trash")
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private var timeFormatter: DateFormatter {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "HH:mm"
+        return formatter
+    }
+}
+
+#Preview {
+    @Previewable @State var previewItems = [
+        TherapySettingItem(time: 0, value: 1.0),
+        TherapySettingItem(time: 1800, value: 1.2)
+    ]
+
+    TimeValueEditorView(
+        items: $previewItems,
+        unit: "U/h",
+        valueOptions: stride(from: 0.0, through: 10.0, by: 0.05).map { Decimal(round(100 * $0) / 100) }
+    )
+}

+ 35 - 34
Trio/Sources/Modules/Onboarding/View/OnboardingView.swift

@@ -37,44 +37,45 @@ extension Onboarding {
                         ScrollView {
                             VStack(alignment: .leading, spacing: 20) {
                                 // Header
-                                HStack {
-                                    Image(systemName: currentStep.iconName)
-                                        .font(.system(size: 40))
-                                        .foregroundColor(currentStep.accentColor)
-                                        .frame(width: 60, height: 60)
-                                        .background(
-                                            Circle()
-                                                .fill(currentStep.accentColor.opacity(0.2))
-                                        )
-
-                                    VStack(alignment: .leading) {
-                                        Text(currentStep.title)
-                                            .font(.largeTitle)
-                                            .fontWeight(.bold)
-                                            .foregroundColor(.primary)
-
-                                        Text(currentStep.description)
-                                            .font(.subheadline)
-                                            .foregroundColor(.secondary)
-                                            .fixedSize(horizontal: false, vertical: true)
+                                if currentStep != .welcome {
+                                    HStack {
+                                        Image(systemName: currentStep.iconName)
+                                            .font(.system(size: 40))
+                                            .foregroundColor(currentStep.accentColor)
+                                            .frame(width: 60, height: 60)
+                                            .background(
+                                                Circle()
+                                                    .fill(currentStep.accentColor.opacity(0.2))
+                                            )
+
+                                        VStack(alignment: .leading) {
+                                            Text(currentStep.title)
+                                                .font(.largeTitle)
+                                                .fontWeight(.bold)
+                                                .foregroundColor(.primary)
+
+                                            Text(currentStep.description)
+                                                .font(.subheadline)
+                                                .foregroundColor(.secondary)
+                                                .fixedSize(horizontal: false, vertical: true)
+                                        }
                                     }
+                                    .padding([.horizontal, .top])
                                 }
-                                .padding(.horizontal)
-                                .padding(.top)
 
                                 // Animation container (for steps that include animations)
-                                AnimationPlaceholder(for: currentStep)
-                                    .padding()
-                                    .scaleEffect(animationScale)
-                                    .opacity(animationOpacity)
-                                    .onAppear {
-                                        withAnimation(.easeInOut(duration: 0.7)) {
-                                            animationOpacity = 1
-                                            animationScale = 1.0
-                                        }
-                                        // Start pulse animation
-                                        isAnimating = true
-                                    }
+//                                AnimationPlaceholder(for: currentStep)
+//                                    .padding()
+//                                    .scaleEffect(animationScale)
+//                                    .opacity(animationOpacity)
+//                                    .onAppear {
+//                                        withAnimation(.easeInOut(duration: 0.7)) {
+//                                            animationOpacity = 1
+//                                            animationScale = 1.0
+//                                        }
+//                                        // Start pulse animation
+//                                        isAnimating = true
+//                                    }
 
                                 // Step-specific content
                                 Group {