فهرست منبع

Merge pull request #189 from dnzxy/touchtarget-ui-refinements

Touch Target UI Refinements
polscm32 1 سال پیش
والد
کامیت
0be875236b

+ 7 - 0
FreeAPS/Sources/Modules/BasalProfileEditor/BasalProfileEditorStateModel.swift

@@ -4,6 +4,7 @@ import SwiftUI
 extension BasalProfileEditor {
     @Observable final class StateModel: BaseStateModel<Provider> {
         @ObservationIgnored @Injected() private var nightscout: NightscoutManager!
+        @ObservationIgnored @Injected() private var broadcaster: Broadcaster!
 
         var syncInProgress: Bool = false
         var initialItems: [Item] = []
@@ -105,6 +106,12 @@ extension BasalProfileEditor {
                     print("We were successful")
                 }
                 .store(in: &lifetime)
+
+            DispatchQueue.main.async {
+                self.broadcaster.notify(BasalProfileObserver.self, on: .main) {
+                    $0.basalProfileDidChange(profile)
+                }
+            }
         }
 
         @MainActor func validate() {

+ 0 - 12
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -110,18 +110,6 @@ extension DataTable {
                 .navigationTitle("History")
                 .navigationBarTitleDisplayMode(.large)
                 .toolbar {
-                    ToolbarItem(placement: .topBarLeading, content: {
-                        Button(
-                            action: { state.showModal(for: .statistics) },
-                            label: {
-                                HStack {
-                                    Text("Statistics")
-                                }
-                            }
-                        )
-                    })
-                }
-                .toolbar {
                     ToolbarItem(placement: .topBarTrailing, content: {
                         addButton({
                             showManualGlucose = true

+ 0 - 2
FreeAPS/Sources/Modules/Home/HomeDataFlow.swift

@@ -9,8 +9,6 @@ protocol HomeProvider: Provider {
     func heartbeatNow()
     func pumpSettings() async -> PumpSettings
     func getBasalProfile() async -> [BasalProfileEntry]
-    func tempTargets(hours: Int) -> [TempTarget]
     func pumpReservoir() async -> Decimal?
-    func tempTarget() -> TempTarget?
     func getBGTargets() async -> BGTargets
 }

+ 0 - 10
FreeAPS/Sources/Modules/Home/HomeProvider.swift

@@ -16,16 +16,6 @@ extension Home {
             apsManager.heartbeat(date: Date())
         }
 
-        func tempTargets(hours: Int) -> [TempTarget] {
-            tempTargetsStorage.recent().filter {
-                $0.createdAt.addingTimeInterval(hours.hours.timeInterval) > Date()
-            }
-        }
-
-        func tempTarget() -> TempTarget? {
-            tempTargetsStorage.current()
-        }
-
         func pumpSettings() async -> PumpSettings {
             await storage.retrieveAsync(OpenAPS.Settings.settings, as: PumpSettings.self)
                 ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))

+ 0 - 14
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -26,7 +26,6 @@ extension Home {
         var basalProfile: [BasalProfileEntry] = []
         var bgTargets = BGTargets(from: OpenAPS.defaults(for: OpenAPS.Settings.bgTargets))
             ?? BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: [])
-        var tempTargets: [TempTarget] = []
         var timerDate = Date()
         var closedLoop = false
         var pumpSuspended = false
@@ -37,7 +36,6 @@ extension Home {
         var reservoir: Decimal?
         var pumpName = ""
         var pumpExpiresAtDate: Date?
-        var tempTarget: TempTarget?
         var highTTraisesSens: Bool = false
         var lowTTlowersSens: Bool = false
         var isExerciseModeActive: Bool = false
@@ -267,7 +265,6 @@ extension Home {
         }
 
         private func registerObservers() {
-            broadcaster.register(GlucoseObserver.self, observer: self)
             broadcaster.register(DeterminationObserver.self, observer: self)
             broadcaster.register(SettingsObserver.self, observer: self)
             broadcaster.register(PreferencesObserver.self, observer: self)
@@ -554,7 +551,6 @@ extension Home {
 }
 
 extension Home.StateModel:
-    GlucoseObserver,
     DeterminationObserver,
     SettingsObserver,
     PreferencesObserver,
@@ -565,11 +561,6 @@ extension Home.StateModel:
     PumpTimeZoneObserver,
     PumpDeactivatedObserver
 {
-    // TODO: still needed?
-    func glucoseDidUpdate(_: [BloodGlucose]) {
-//        setupGlucose()
-    }
-
     func determinationDidUpdate(_: Determination) {
         waitForSuggestion = false
     }
@@ -606,11 +597,6 @@ extension Home.StateModel:
         lowTTlowersSens = settingsManager.preferences.lowTemptargetLowersSensitivity
     }
 
-    // TODO: is this ever really triggered? react to MOC changes?
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        displayPumpStatusHighlightMessage()
-    }
-
     func pumpSettingsDidChange(_: PumpSettings) {
         Task {
             await setupPumpSettings()

+ 5 - 10
FreeAPS/Sources/Modules/Home/View/Chart/ChartElements/GlucoseTargetsView.swift

@@ -47,18 +47,13 @@ struct GlucoseTargetsView: ChartContent {
      Processes raw glucose target data into a list of target profiles for visualization.
 
      - Parameter rawTargets: The raw glucose target data containing offset and glucose values.
-     - Returns: An array of `TargetProfile` objects, each representing a glucose target range for today and tomorrow.
+     - Returns: An array of `TargetProfile` objects, each representing a glucose target range starting from the day of the startMarker and ending two days later.
 
      The function:
-     - Converts glucose targets into profiles covering two consecutive days (today and tomorrow).
+     - Converts glucose targets into profiles covering three consecutive days (day of startMarker, day after startMarker and day after that).
      - Calculates start and end times for each target based on the offsets provided.
      - Handles conversions between mg/dL and mmol/L as per user settings.
      - Ensures targets span across midnight to avoid data cutoff.
-
-     Example:
-     For a target at offset 0 (midnight) with low glucose value 70 mg/dL, the function generates two profiles:
-     - One for today from midnight to the next target offset or end of the day.
-     - Another for tomorrow covering the same time range.
      */
     private func processFetchedTargets(_ rawTargets: BGTargets) -> [TargetProfile] {
         var targetProfiles: [TargetProfile] = []
@@ -74,9 +69,9 @@ struct GlucoseTargetsView: ChartContent {
         // Base date is the start of the day for the startMarker
         let baseDate = Calendar.current.startOfDay(for: startMarker)
 
-        // Process each target twice: once for today and once for tomorrow
-        for index in 0 ..< (targets.count * 2) {
-            // Calculate the day offset (0 for today, 1 for tomorrow)
+        // Process each target three times
+        for index in 0 ..< (targets.count * 3) {
+            // Calculate the day offset (0 for today, 1 for tomorrow, 2 for day after)
             let dayOffset = index / targets.count
             let targetIndex = index % targets.count
 

+ 0 - 1
FreeAPS/Sources/Modules/Home/View/Chart/MainChartView.swift

@@ -10,7 +10,6 @@ struct MainChartView: View {
     var safeAreaSize: CGFloat
     var units: GlucoseUnits
     var hours: Int
-    var tempTargets: [TempTarget]
     var highGlucose: Decimal
     var lowGlucose: Decimal
     var currentGlucoseTarget: Decimal

+ 13 - 11
FreeAPS/Sources/Modules/Home/View/Header/LoopView.swift

@@ -4,6 +4,8 @@ import SwiftUI
 import UIKit
 
 struct LoopView: View {
+    @Environment(\.colorScheme) var colorScheme
+
     private enum Config {
         static let lag: TimeInterval = 30
     }
@@ -19,10 +21,19 @@ struct LoopView: View {
     private let rect = CGRect(x: 0, y: 0, width: 18, height: 18)
 
     var body: some View {
+        loopStatusWithMinutes
+            .padding(.vertical, 5)
+            .padding(.horizontal, 10)
+            .overlay(
+                Capsule()
+                    .stroke(color.opacity(0.4), lineWidth: 2)
+            )
+    }
+
+    private var loopStatusWithMinutes: some View {
         HStack(alignment: .center) {
             ZStack {
-                Image(systemName: "circle")
-                    .mask(mask(in: rect).fill(style: FillStyle(eoFill: true)))
+                Image(systemName: (!closedLoop || manualTempBasal) ? "circle.and.line.horizontal" : "circle")
                 if isLooping {
                     ProgressView()
                 }
@@ -41,7 +52,6 @@ struct LoopView: View {
                 Text("--")
             }
         }
-        .strikethrough(!closedLoop || manualTempBasal, pattern: .solid, color: color)
         .font(.callout).fontWeight(.bold).fontDesign(.rounded)
         .foregroundColor(color)
     }
@@ -80,14 +90,6 @@ struct LoopView: View {
             return .loopRed
         }
     }
-
-    func mask(in rect: CGRect) -> Path {
-        var path = Rectangle().path(in: rect)
-        if !closedLoop || manualTempBasal {
-            path.addPath(Rectangle().path(in: CGRect(x: rect.minX, y: rect.midY - 4, width: rect.width, height: 8)))
-        }
-        return path
-    }
 }
 
 extension View {

+ 40 - 18
FreeAPS/Sources/Modules/Home/View/Header/PumpView.swift

@@ -45,24 +45,44 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "cross.vial.fill")
                             .font(.callout)
-                            .foregroundColor(reservoirColor)
+
                         if reservoir == 0xDEAD_BEEF {
                             Text("50+ " + NSLocalizedString("U", comment: "Insulin unit"))
-                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                .font(.callout)
+                                .fontWeight(.bold)
+                                .fontDesign(.rounded)
                         } else {
                             Text(
                                 Formatter.integerFormatter
                                     .string(from: reservoir as NSNumber)! + NSLocalizedString(" U", comment: "Insulin unit")
                             )
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                            .font(.callout)
+                            .fontWeight(.bold)
+                            .fontDesign(.rounded)
                         }
                     }
+                    .padding(.vertical, 5)
+                    .padding(.horizontal, 10)
+                    .foregroundStyle(reservoirColor)
+                    .overlay(
+                        Capsule()
+                            .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
+                    )
 
                     if let timeZone = timeZone, timeZone.secondsFromGMT() != TimeZone.current.secondsFromGMT() {
-                        Image(systemName: "clock.badge.exclamationmark.fill")
-                            .font(.callout)
-                            .symbolRenderingMode(.palette)
-                            .foregroundStyle(.red, Color(.warning))
+                        HStack {
+                            Image(systemName: "clock.badge.exclamationmark.fill")
+                                .font(.callout)
+                                .symbolRenderingMode(.palette)
+                                .foregroundStyle(.red, Color(.warning))
+
+                            Text("Timezone")
+                                .font(.callout)
+                                .fontWeight(.bold)
+                                .fontDesign(.rounded)
+                                .foregroundStyle(.red)
+                        }
+                        .padding(.leading, 12)
                     }
                 }
 
@@ -70,7 +90,7 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "battery.100")
                             .font(.callout)
-                            .foregroundColor(batteryColor)
+                            .foregroundStyle(batteryColor)
                         Text("\(Int(battery.first?.percent ?? 100)) %")
                             .font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
@@ -80,13 +100,15 @@ struct PumpView: View {
                     HStack {
                         Image(systemName: "stopwatch.fill")
                             .font(.callout)
-                            .foregroundColor(timerColor)
+                            .foregroundStyle(timerColor)
 
                         Text(remainingTimeString(time: date.timeIntervalSince(timerDate)))
                             .font(!(date.timeIntervalSince(timerDate) > 0) ? .subheadline : .callout)
                             .fontWeight(.bold)
                             .fontDesign(.rounded)
                     }
+                    // aligns the stopwatch icon exactly with the first pixel of the reservoir icon
+                    .padding(.leading, 12)
                 }
             }
         }
@@ -123,11 +145,11 @@ struct PumpView: View {
 
         switch battery.percent {
         case ...10:
-            return .red
+            return Color.loopRed
         case ...20:
-            return .yellow
+            return Color.orange
         default:
-            return .green
+            return Color.loopGreen
         }
     }
 
@@ -138,11 +160,11 @@ struct PumpView: View {
 
         switch reservoir {
         case ...10:
-            return .red
+            return Color.loopRed
         case ...30:
-            return .yellow
+            return Color.orange
         default:
-            return .blue
+            return Color.insulin
         }
     }
 
@@ -155,11 +177,11 @@ struct PumpView: View {
 
         switch time {
         case ...8.hours.timeInterval:
-            return .red
+            return Color.loopRed
         case ...1.days.timeInterval:
-            return .yellow
+            return Color.orange
         default:
-            return .green
+            return Color.loopGreen
         }
     }
 }

+ 107 - 51
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -5,11 +5,9 @@ import SwiftUI
 import Swinject
 
 struct TimePicker: Identifiable {
-    let label: String
-    let number: String
     var active: Bool
     let hours: Int16
-    var id: String { label }
+    var id: String { hours.description }
 }
 
 extension Home {
@@ -36,15 +34,12 @@ extension Home {
         @State var showPumpSelection: Bool = false
         @State var notificationsDisabled = false
         @State var timeButtons: [TimePicker] = [
-            TimePicker(label: "2 hours", number: "2", active: false, hours: 2),
-            TimePicker(label: "4 hours", number: "4", active: false, hours: 4),
-            TimePicker(label: "6 hours", number: "6", active: false, hours: 6),
-            TimePicker(label: "12 hours", number: "12", active: false, hours: 12),
-            TimePicker(label: "24 hours", number: "24", active: false, hours: 24)
+            TimePicker(active: false, hours: 4),
+            TimePicker(active: false, hours: 6),
+            TimePicker(active: false, hours: 12),
+            TimePicker(active: false, hours: 24)
         ]
 
-        let buttonFont = Font.custom("TimeButtonFont", size: 14)
-
         @FetchRequest(fetchRequest: OverrideStored.fetch(
             NSPredicate.lastActiveOverride,
             ascending: false,
@@ -116,7 +111,8 @@ extension Home {
                 timeZone: state.timeZone,
                 pumpStatusHighlightMessage: state.pumpStatusHighlightMessage,
                 battery: state.batteryFromPersistence
-            ).onTapGesture {
+            )
+            .onTapGesture {
                 if state.pumpDisplayState == nil {
                     // shows user confirmation dialog with pump model choices, then proceeds to setup
                     showPumpSelection.toggle()
@@ -245,45 +241,73 @@ extension Home {
             return components.isEmpty ? nil : components.joined(separator: ", ")
         }
 
-        var timeInterval: some View {
-            HStack(alignment: .center) {
+        var timeIntervalButtons: some View {
+            let buttonColor = (colorScheme == .dark ? Color.white : Color.black).opacity(0.8)
+
+            return HStack(alignment: .center) {
                 ForEach(timeButtons) { button in
-                    Text(button.active ? NSLocalizedString(button.label, comment: "") : button.number).onTapGesture {
+                    Button(action: {
                         state.hours = button.hours
+                    }) {
+                        Group {
+                            if button.active {
+                                Text(
+                                    NSLocalizedString(button.hours.description, comment: "") + " " +
+                                        NSLocalizedString("h", comment: "h")
+                                )
+                            } else {
+                                Text(NSLocalizedString(button.hours.description, comment: ""))
+                            }
+                        }
+                        .font(.footnote)
+                        .fontWeight(button.active ? .semibold : .regular)
+                        .padding(.vertical, 5)
+                        .padding(.horizontal, 10)
+                        .foregroundColor(
+                            button
+                                .active ? (colorScheme == .dark ? Color.bgDarkerDarkBlue : Color.white) : buttonColor
+                        )
+                        .background(button.active ? buttonColor.opacity(colorScheme == .dark ? 1 : 0.8) : Color.clear)
+                        .clipShape(Capsule())
+                        .overlay(
+                            Capsule()
+                                .stroke(button.active ? buttonColor.opacity(0.4) : Color.clear, lineWidth: 2)
+                        )
                     }
-                    .foregroundStyle(button.active ? (colorScheme == .dark ? Color.white : Color.black).opacity(0.9) : .secondary)
-                    .frame(maxHeight: 30).padding(.horizontal, 8)
-                    .background(
-                        button.active ?
-                            // RGB(30, 60, 95)
-                            (
-                                colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
-                                    Color.white
-                            ) :
-                            Color
-                            .clear
-                    )
-                    .cornerRadius(20)
                 }
-                Button(action: {
-                    state.isLegendPresented.toggle()
-                }) {
-                    Image(systemName: "info")
-                        .foregroundColor(colorScheme == .dark ? Color.white : Color.black).opacity(0.9)
-                        .frame(width: 20, height: 20)
-                        .background(
-                            colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
-                                Color.white
-                        )
-                        .clipShape(Circle())
+            }
+        }
+
+        var statsIconString: String {
+            if #available(iOS 18, *) {
+                return "chart.line.text.clipboard"
+            } else {
+                return "list.clipboard"
+            }
+        }
+
+        @ViewBuilder private func tappableButton(
+            buttonColor: Color,
+            label: String,
+            iconString: String,
+            action: @escaping () -> Void
+        ) -> some View {
+            Button(action: {
+                action()
+            }) {
+                HStack {
+                    Image(systemName: iconString)
+                    Text(label)
                 }
-                .padding([.top, .bottom])
+                .font(.footnote)
+                .padding(.vertical, 5)
+                .padding(.horizontal, 10)
+                .foregroundStyle(buttonColor)
+                .overlay(
+                    Capsule()
+                        .stroke(buttonColor.opacity(0.4), lineWidth: 2)
+                )
             }
-            .shadow(
-                color: Color.black.opacity(colorScheme == .dark ? 0.75 : 0.33),
-                radius: colorScheme == .dark ? 5 : 3
-            )
-            .font(buttonFont)
         }
 
         @ViewBuilder func mainChart(geo: GeometryProxy) -> some View {
@@ -293,7 +317,6 @@ extension Home {
                     safeAreaSize: notificationsDisabled == true ? safeAreaSize : 0,
                     units: state.units,
                     hours: state.filteredHours,
-                    tempTargets: state.tempTargets,
                     highGlucose: state.highGlucose,
                     lowGlucose: state.lowGlucose,
                     currentGlucoseTarget: state.currentGlucoseTarget,
@@ -324,9 +347,11 @@ extension Home {
                     lastLoopDate: state.lastLoopDate,
                     manualTempBasal: state.manualTempBasal,
                     determination: state.determinationsFromPersistence
-                ).onTapGesture {
+                )
+                .onTapGesture {
                     state.isLoopStatusPresented = true
-                }.onLongPressGesture {
+                }
+                .onLongPressGesture {
                     let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
                     impactHeavy.impactOccurred()
                     state.runLoop()
@@ -347,6 +372,8 @@ extension Home {
                             )!
                         ).font(.callout).fontWeight(.bold).fontDesign(.rounded)
                     }
+                    // aligns the evBG icon exactly with the first pixel of loop status icon
+                    .padding(.leading, 12)
                 } else {
                     HStack {
                         Image(systemName: "arrow.right.circle")
@@ -402,8 +429,17 @@ extension Home {
                         Image(systemName: "drop.circle")
                             .font(.callout)
                             .foregroundColor(.insulinTintColor)
-                        Text(tempBasalString)
-                            .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                        if tempBasalString.count > 5 {
+                            Text(tempBasalString)
+                                .font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                                .lineLimit(1)
+                                .minimumScaleFactor(0.85)
+                                .truncationMode(.tail)
+                                .allowsTightening(true)
+                        } else {
+                            // Short strings can just display normally
+                            Text(tempBasalString).font(.callout).fontWeight(.bold).fontDesign(.rounded)
+                        }
                     } else {
                         Image(systemName: "drop.circle")
                             .font(.callout)
@@ -815,8 +851,28 @@ extension Home {
 
                 mainChart(geo: geo)
 
-                timeInterval.padding(.top, UIDevice.adjustPadding(min: 0, max: 12))
-                    .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 12))
+                HStack {
+                    tappableButton(
+                        buttonColor: (colorScheme == .dark ? Color.white : Color.black).opacity(0.8),
+                        label: "Stats",
+                        iconString: statsIconString,
+                        action: { state.showModal(for: .statistics) }
+                    )
+
+                    Spacer()
+
+                    timeIntervalButtons.padding(.top, UIDevice.adjustPadding(min: 0, max: 10))
+                        .padding(.bottom, UIDevice.adjustPadding(min: 0, max: 10))
+
+                    Spacer()
+
+                    tappableButton(
+                        buttonColor: (colorScheme == .dark ? Color.white : Color.black).opacity(0.8),
+                        label: "Info",
+                        iconString: "info",
+                        action: { state.isLegendPresented.toggle() }
+                    )
+                }.padding([.horizontal, .top, .bottom])
 
                 if let progress = state.bolusProgress {
                     bolusView(geo: geo, progress)

+ 7 - 0
FreeAPS/Sources/Modules/TargetsEditor/TargetsEditorStateModel.swift

@@ -3,6 +3,7 @@ import SwiftUI
 extension TargetsEditor {
     final class StateModel: BaseStateModel<Provider> {
         @Injected() private var nightscout: NightscoutManager!
+        @Injected() private var broadcaster: Broadcaster!
 
         @Published var items: [Item] = []
         @Published var initialItems: [Item] = []
@@ -75,6 +76,12 @@ extension TargetsEditor {
             provider.saveProfile(profile)
             initialItems = items.map { Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
 
+            DispatchQueue.main.async {
+                self.broadcaster.notify(BGTargetsObserver.self, on: .main) {
+                    $0.bgTargetsDidChange(profile)
+                }
+            }
+
             Task.detached(priority: .low) {
                 debug(.nightscout, "Attempting to upload targets to Nightscout")
                 await self.nightscout.uploadProfiles()