Преглед изворни кода

Merge branch 'dev' into therapy

Deniz Cengiz пре 6 месеци
родитељ
комит
9e0ed896c7

+ 1 - 1
Config.xcconfig

@@ -19,7 +19,7 @@ TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 // The developers set the version numbers, please leave them alone
 APP_VERSION = 0.6.0
-APP_DEV_VERSION = 0.6.0.5
+APP_DEV_VERSION = 0.6.0.7
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 12 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -86520,6 +86520,9 @@
         }
       }
     },
+    "Deselect All" : {
+
+    },
     "Detailed" : {
       "localizations" : {
         "bg" : {
@@ -107953,6 +107956,9 @@
         }
       }
     },
+    "External Bolus" : {
+
+    },
     "External Insulin" : {
       "comment" : "A manually entered dose of external insulin",
       "localizations" : {
@@ -111941,6 +111947,9 @@
         }
       }
     },
+    "Filter" : {
+
+    },
     "Fine-tune the ring’s Width and Gap to suit your design preferences." : {
       "localizations" : {
         "bg" : {
@@ -200469,6 +200478,9 @@
         }
       }
     },
+    "Select All" : {
+
+    },
     "Select CGM Model" : {
       "localizations" : {
         "bg" : {

+ 26 - 0
Trio/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -6,6 +6,32 @@ import SwiftUI
 enum DataTable {
     enum Config {}
 
+    enum TreatmentType: String, CaseIterable {
+        case bolus = "Bolus"
+        case externalBolus = "External Bolus"
+        case smb = "SMB"
+        case tempBasal = "Temp Basal"
+        case suspend = "Suspend"
+        case other = "Other"
+
+        var displayName: String {
+            switch self {
+            case .bolus:
+                return String(localized: "Bolus")
+            case .externalBolus:
+                return String(localized: "External Bolus")
+            case .smb:
+                return String(localized: "SMB")
+            case .tempBasal:
+                return String(localized: "Temp Basal")
+            case .suspend:
+                return String(localized: "Suspend")
+            case .other:
+                return String(localized: "Other")
+            }
+        }
+    }
+
     enum Mode: String, Hashable, Identifiable, CaseIterable {
         case treatments
         case meals

+ 130 - 4
Trio/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -18,6 +18,9 @@ extension DataTable {
         @State private var showFutureEntries: Bool = false // default to hide future entries
         @State private var showManualGlucose: Bool = false
         @State private var isAmountUnconfirmed: Bool = true
+        @State private var showTreatmentTypeFilter = false
+        @State private var selectedTreatmentTypes: Set<TreatmentType> = Set(TreatmentType.allCases)
+        @State private var filterPopoverAnchor: CGRect = .zero
 
         @Environment(\.colorScheme) var colorScheme
         @Environment(\.managedObjectContext) var context
@@ -169,15 +172,138 @@ extension DataTable {
             ).buttonStyle(.borderless)
         }
 
+        private var filterTreatmentsButton: some View {
+            Button(action: {
+                showTreatmentTypeFilter.toggle()
+            }) {
+                HStack {
+                    Text("Filter")
+                    Image(
+                        systemName: selectedTreatmentTypes.count == TreatmentType.allCases.count
+                            ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill"
+                    )
+                    if selectedTreatmentTypes.count < TreatmentType.allCases.count {
+                        Text(verbatim: "(\(selectedTreatmentTypes.count)/\(TreatmentType.allCases.count))")
+                    }
+                }.foregroundColor(Color.accentColor)
+            }
+            .popover(isPresented: $showTreatmentTypeFilter, arrowEdge: .top) {
+                VStack(alignment: .leading, spacing: 20) {
+                    Button(action: {
+                        if selectedTreatmentTypes.count == TreatmentType.allCases.count {
+                            // Deselect all - keep at least one selected
+                            selectedTreatmentTypes = []
+                        } else {
+                            // Select all
+                            selectedTreatmentTypes = Set(TreatmentType.allCases)
+                        }
+                    }) {
+                        HStack(spacing: 20) {
+                            Image(
+                                systemName: selectedTreatmentTypes.count == TreatmentType.allCases.count
+                                    ? "checkmark.circle.fill" : "circle"
+                            )
+                            .frame(width: 20)
+                            .foregroundColor(Color.accentColor)
+                            Text(selectedTreatmentTypes.count == TreatmentType.allCases.count ? "Deselect All" : "Select All")
+                                .foregroundColor(Color.primary)
+                        }.padding(4)
+                    }
+                    .buttonStyle(.borderless)
+
+                    Divider()
+
+                    ForEach(TreatmentType.allCases, id: \.rawValue) { treatmentType in
+                        Button(action: {
+                            toggleTreatmentType(treatmentType)
+                        }) {
+                            HStack(spacing: 20) {
+                                Image(
+                                    systemName: selectedTreatmentTypes
+                                        .contains(treatmentType) ? "checkmark.circle.fill" : "circle"
+                                )
+                                .frame(width: 20)
+                                .foregroundColor(Color.accentColor)
+                                Text(treatmentType.displayName)
+                                    .foregroundColor(Color.primary)
+                            }.padding(4)
+                        }
+                        .buttonStyle(.borderless)
+                    }
+
+                    Divider()
+
+                    Button("Done") {
+                        showTreatmentTypeFilter = false
+                    }
+                    .frame(maxWidth: .infinity)
+                    .buttonStyle(.borderless)
+                }
+                .padding()
+                .presentationCompactAdaptation(.popover)
+                .background(Color.chart)
+            }
+        }
+
+        private var filterFutureEntriesButton: some View {
+            Button(
+                action: {
+                    showFutureEntries.toggle()
+                },
+                label: {
+                    HStack {
+                        Text(showFutureEntries ? "Hide Future" : "Show Future")
+                            .foregroundColor(Color.accentColor)
+                        Image(systemName: showFutureEntries ? "eye.slash" : "eye")
+                            .foregroundColor(Color.accentColor)
+                    }
+                }
+            ).buttonStyle(.borderless)
+        }
+
+        private func toggleTreatmentType(_ type: TreatmentType) {
+            if selectedTreatmentTypes.contains(type) {
+                selectedTreatmentTypes.remove(type)
+            } else {
+                selectedTreatmentTypes.insert(type)
+            }
+        }
+
+        private var filteredPumpEvents: [PumpEventStored] {
+            pumpEventStored.filter { item in
+                // First filter by date
+                let passesDateFilter = !showFutureEntries ? item.timestamp ?? Date() <= Date() : true
+
+                guard passesDateFilter else { return false }
+
+                // Then filter by treatment type
+                if let bolus = item.bolus {
+                    if bolus.isSMB {
+                        return selectedTreatmentTypes.contains(.smb)
+                    } else if bolus.isExternal {
+                        return selectedTreatmentTypes.contains(.externalBolus)
+                    } else {
+                        return selectedTreatmentTypes.contains(.bolus)
+                    }
+                } else if item.tempBasal != nil {
+                    return selectedTreatmentTypes.contains(.tempBasal)
+                } else if item.type == "PumpSuspend" {
+                    return selectedTreatmentTypes.contains(.suspend)
+                } else {
+                    return selectedTreatmentTypes.contains(.other)
+                }
+            }
+        }
+
         private var treatmentsList: some View {
             List {
                 HStack {
-                    Text("Insulin").foregroundStyle(.secondary)
+                    filterTreatmentsButton
                     Spacer()
                     Text("Time").foregroundStyle(.secondary)
                 }
-                if !pumpEventStored.isEmpty {
-                    ForEach(pumpEventStored.filter({ !showFutureEntries ? $0.timestamp ?? Date() <= Date() : true })) { item in
+                if !filteredPumpEvents.isEmpty {
+                    ForEach(filteredPumpEvents) { item in
                         treatmentView(item)
                     }
                 } else {
@@ -194,7 +320,7 @@ extension DataTable {
                 HStack {
                     Text("Type").foregroundStyle(.secondary)
                     Spacer()
-                    filterEntriesButton
+                    filterFutureEntriesButton
                 }
                 if !carbEntryStored.isEmpty {
                     ForEach(carbEntryStored.filter({ !showFutureEntries ? $0.date ?? Date() <= Date() : true })) { item in

+ 18 - 13
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/BolusStatsView.swift

@@ -327,29 +327,32 @@ private struct BolusSelectionPopover: View {
     }
 
     private func xOffset() -> CGFloat {
+        // If the selected date is outside the visible domain, hide the popover
+        guard selectedDate >= domain.start && selectedDate <= domain.end else { return 0 }
+        
         let domainDuration = domain.end.timeIntervalSince(domain.start)
         guard domainDuration > 0, chartWidth > 0 else { return 0 }
 
         let popoverWidth = popoverSize.width
+        let padding: CGFloat = 10 // Padding from screen edges
 
-        // Convert dates to pixel'd x-condition
+        // Convert dates to pixel'd x-position
         let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
         let x_selected = dateFraction * chartWidth
 
-        // TODO: this is semi hacky, can this be improved?
-        let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
-        let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
+        // Calculate popover edges
+        let x_left = x_selected - (popoverWidth / 2)
+        let x_right = x_selected + (popoverWidth / 2)
 
-        var offset: CGFloat = 0 // Default = no shift
+        var offset: CGFloat = 0
 
-        // Push popover to right if its left edge is (nearing) out-of-bounds
-        if x_left < 0 {
-            offset = abs(x_left) // push to right
-        }
-
-        // Push popover to left if its right edge is (nearing) out-of-bounds)
-        if x_right > chartWidth {
-            offset = -(x_right - chartWidth) // push to left
+        // Ensure the popover stays within screen bounds
+        if x_left < padding {
+            // Popover would extend past left edge, shift it right
+            offset = padding - x_left
+        } else if x_right > chartWidth - padding {
+            // Popover would extend past right edge, shift it left
+            offset = (chartWidth - padding) - x_right
         }
 
         return offset
@@ -413,5 +416,7 @@ private struct BolusSelectionPopover: View {
         )
         // Apply calculated xOffset to keep within bounds
         .offset(x: xOffset(), y: 0)
+        // Hide popover if selected date is outside visible domain
+        .opacity(selectedDate >= domain.start && selectedDate <= domain.end ? 1 : 0)
     }
 }

+ 18 - 13
Trio/Sources/Modules/Stat/View/ViewElements/Insulin/TotalDailyDoseChart.swift

@@ -282,29 +282,32 @@ private struct TDDSelectionPopover: View {
     }
 
     private func xOffset() -> CGFloat {
+        // If the selected date is outside the visible domain, hide the popover
+        guard selectedDate >= domain.start && selectedDate <= domain.end else { return 0 }
+
         let domainDuration = domain.end.timeIntervalSince(domain.start)
         guard domainDuration > 0, chartWidth > 0 else { return 0 }
 
         let popoverWidth = popoverSize.width
+        let padding: CGFloat = 10 // Padding from screen edges
 
-        // Convert dates to pixel'd x-condition
+        // Convert dates to pixel'd x-position
         let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
         let x_selected = dateFraction * chartWidth
 
-        // TODO: this is semi hacky, can this be improved?
-        let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
-        let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
-
-        var offset: CGFloat = 0 // Default = no shift
+        // Calculate popover edges
+        let x_left = x_selected - (popoverWidth / 2)
+        let x_right = x_selected + (popoverWidth / 2)
 
-        // Push popover to right if its left edge is (nearing) out-of-bounds
-        if x_left < 0 {
-            offset = abs(x_left) // push to right
-        }
+        var offset: CGFloat = 0
 
-        // Push popover to left if its right edge is (nearing) out-of-bounds)
-        if x_right > chartWidth {
-            offset = -(x_right - chartWidth) // push to left
+        // Ensure the popover stays within screen bounds
+        if x_left < padding {
+            // Popover would extend past left edge, shift it right
+            offset = padding - x_left
+        } else if x_right > chartWidth - padding {
+            // Popover would extend past right edge, shift it left
+            offset = (chartWidth - padding) - x_right
         }
 
         return offset
@@ -345,5 +348,7 @@ private struct TDDSelectionPopover: View {
         )
         // Apply calculated xOffset to keep within bounds
         .offset(x: xOffset(), y: 0)
+        // Hide popover if selected date is outside visible domain
+        .opacity(selectedDate >= domain.start && selectedDate <= domain.end ? 1 : 0)
     }
 }

+ 18 - 13
Trio/Sources/Modules/Stat/View/ViewElements/Meal/MealStatsView.swift

@@ -318,29 +318,32 @@ private struct MealSelectionPopover: View {
     }
 
     private func xOffset() -> CGFloat {
+        // If the selected date is outside the visible domain, hide the popover
+        guard selectedDate >= domain.start && selectedDate <= domain.end else { return 0 }
+
         let domainDuration = domain.end.timeIntervalSince(domain.start)
         guard domainDuration > 0, chartWidth > 0 else { return 0 }
 
         let popoverWidth = popoverSize.width
+        let padding: CGFloat = 10 // Padding from screen edges
 
-        // Convert dates to pixel'd x-condition
+        // Convert dates to pixel'd x-position
         let dateFraction = selectedDate.timeIntervalSince(domain.start) / domainDuration
         let x_selected = dateFraction * chartWidth
 
-        // TODO: this is semi hacky, can this be improved?
-        let x_left = x_selected - (popoverWidth / 2) // Left edge of popover
-        let x_right = x_selected + (popoverWidth / 2) // Right edge of popover
-
-        var offset: CGFloat = 0 // Default = no shift
+        // Calculate popover edges
+        let x_left = x_selected - (popoverWidth / 2)
+        let x_right = x_selected + (popoverWidth / 2)
 
-        // Push popover to right if its left edge is (nearing) out-of-bounds
-        if x_left < 0 {
-            offset = abs(x_left) // push to right
-        }
+        var offset: CGFloat = 0
 
-        // Push popover to left if its right edge is (nearing) out-of-bounds)
-        if x_right > chartWidth {
-            offset = -(x_right - chartWidth) // push to left
+        // Ensure the popover stays within screen bounds
+        if x_left < padding {
+            // Popover would extend past left edge, shift it right
+            offset = padding - x_left
+        } else if x_right > chartWidth - padding {
+            // Popover would extend past right edge, shift it left
+            offset = (chartWidth - padding) - x_right
         }
 
         return offset
@@ -403,5 +406,7 @@ private struct MealSelectionPopover: View {
         )
         // Apply calculated xOffset to keep within bounds
         .offset(x: xOffset(), y: 0)
+        // Hide popover if selected date is outside visible domain
+        .opacity(selectedDate >= domain.start && selectedDate <= domain.end ? 1 : 0)
     }
 }