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

Merge pull request #839 from nightscout/sync-dev-to-medtrum-2025-11-01

Sync dev to medtrum as of 2025-11-01
Sjoerd Bozon пре 6 месеци
родитељ
комит
b76d329538

+ 27 - 11
.github/workflows/build_trio.yml

@@ -2,14 +2,10 @@ name: 4. Build Trio
 run-name: Build Trio (${{ github.ref_name }})
 on:
   workflow_dispatch:
-
-  ## Remove the "#" sign from the beginning of the line below to get automated builds on push (code changes in your repository)
-  #push:
-
   schedule:
-    # avoid starting an action at times when GitHub resources are more likely to be impacted
-    - cron: "43 8 * * 0" # Checks for updates at 08:43 UTC every Sunday
-    - cron: "43 6 8-14 * 6" # Builds the app on the second Saturday of each month at 06:43 UTC
+    # Check for updates every Sunday
+    #   Later logic builds if there are updates or if it is the 2nd Sunday of the month
+    - cron: "43 6 * * 0" # Sunday at UTC 06:43
 
 env:
   UPSTREAM_REPO: nightscout/Trio
@@ -19,6 +15,26 @@ env:
   ALIVE_BRANCH_DEV: alive-dev
 
 jobs:
+
+  # Set a logic flag if this is the second instance of this day-of-week in this month
+  day_in_month:
+    runs-on: ubuntu-latest
+    name: Check day in month
+    outputs:
+      IS_SECOND_IN_MONTH: ${{ steps.date-check.outputs.is_second_instance }}
+
+    steps:
+      - id: date-check
+        name: Check if this is the second time this day-of-week happens this month
+        run: |
+          DAY_OF_MONTH=$(date +%-d)
+          WEEK_OF_MONTH=$(( ($(date +%-d) - 1) / 7 + 1 ))
+          if [[ $WEEK_OF_MONTH -eq 2 ]]; then
+            echo "is_second_instance=true" >> "$GITHUB_OUTPUT"
+          else
+            echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
+          fi
+
   # Checks if Distribution certificate is present and valid, optionally nukes and
   # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
   check_certs:
@@ -205,20 +221,20 @@ jobs:
   # Builds Trio
   build:
     name: Build
-    needs: [check_certs, check_alive_and_permissions, check_latest_from_upstream]
+    needs: [check_certs, check_alive_and_permissions, check_latest_from_upstream, day_in_month]
     runs-on: macos-15
     permissions:
       contents: write
     if:
-      | # runs if started manually, or if sync schedule is set and enabled and scheduled on the first Saturday each month, or if sync schedule is set and enabled and new commits were found
+      | # builds with manual start; if automatic: once a month or when new commits are found
       github.event_name == 'workflow_dispatch' ||
       (needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&
-        (vars.SCHEDULED_BUILD != 'false' && github.event.schedule == '43 6 8-14 * 6') ||
+        (vars.SCHEDULED_BUILD != 'false' && needs.day_in_month.outputs.IS_SECOND_IN_MONTH == 'true') ||
         (vars.SCHEDULED_SYNC != 'false' && needs.check_latest_from_upstream.outputs.NEW_COMMITS == 'true' )
       )
     steps:
       - name: Select Xcode version
-        run: "sudo xcode-select --switch /Applications/Xcode_16.3.app/Contents/Developer"
+        run: "sudo xcode-select --switch /Applications/Xcode_16.4.app/Contents/Developer"
       
       - name: Checkout Repo for syncing
         if: |

+ 1 - 1
CGMBLEKit

@@ -1 +1 @@
-Subproject commit b583df15f633fd29ad85ef22001af259cb544a75
+Subproject commit 1cc9e9e7627cf8fb76ccdb015dd6991196038e31

+ 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.1
+APP_DEV_VERSION = 0.6.0.7
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit 3e606b8e12d08d27a5942e7f4af9a07b642b676f
+Subproject commit 084de69f69b1b17c92b595b4d5afeaed5b5d1e55

+ 1 - 1
G7SensorKit

@@ -1 +1 @@
-Subproject commit 84c5f4ea27fb445ed3e2108044569c8d881bc50b
+Subproject commit 9fa27889f0b216cbe0a23844e888de6698793b63

+ 1 - 1
LibreTransmitter

@@ -1 +1 @@
-Subproject commit 709b22ea1a2ac1a096c9a7bb3d2e65f7839c2d51
+Subproject commit 1950f1fec2a0e9f256c1be6e5bafd06ff79d3144

+ 1 - 1
LoopKit

@@ -1 +1 @@
-Subproject commit c2b0dd503cfbe4c056e61a6d6842d28f5c66af9a
+Subproject commit ddee20aca806f7635b8421617a675ddbd9c6d924

+ 1 - 1
MinimedKit

@@ -1 +1 @@
-Subproject commit d367254415691ef1835cfd70a3af390b0a6aad81
+Subproject commit a1888623f398994e07ad970a0164be1117e9bec1

+ 1 - 1
OmniBLE

@@ -1 +1 @@
-Subproject commit 99ded91567594c9ce0d5d0a86335670085ad5764
+Subproject commit e4378ba744a46c5f06f9507eabceb4072c058992

+ 1 - 1
OmniKit

@@ -1 +1 @@
-Subproject commit 858618d2ed8fe5779152a8ce55e716fa1921e95d
+Subproject commit 1be14fcc27f22258cf8daa0355ac70c89737c0cc

+ 1 - 1
RileyLinkKit

@@ -1 +1 @@
-Subproject commit 2ab3daa50553b8886b411f67d9773a2f32fc7e8c
+Subproject commit b280e8b9b7e75674b763f3ebf96d8b21dddcf80a

+ 1 - 1
TidepoolService

@@ -1 +1 @@
-Subproject commit a25223890bf60e35f02a7096d1d2f76268d42930
+Subproject commit 59b0cd9384d180c7cccaf2cd2416fa2592a0ce45

Разлика између датотеке није приказан због своје велике величине
+ 32938 - 3312
Trio/Sources/Localizations/Main/Localizable.xcstrings


+ 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

+ 3 - 3
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/CarbRatioStepView.swift

@@ -69,7 +69,7 @@ struct CarbRatioStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 8) {
-                            Text("For 45g of carbs, you would need:")
+                            Text("For 45 g of carbs, you would need:")
                                 .font(.subheadline)
                                 .padding(.horizontal)
 
@@ -79,7 +79,7 @@ struct CarbRatioStepView: View {
                                         .carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber
                                 )
                             Text(
-                                "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--")  = \(String(format: "%.1f", insulinNeeded))" +
+                                "45 \(String(localized: "g", comment: "Gram abbreviation")) / \(formatter.string(from: state.carbRatioRateValues[state.carbRatioItems.first!.rateIndex] as NSNumber) ?? "--") \(String(localized: "g/U")) = \(String(format: "%.1f", insulinNeeded))" +
                                     " " + String(localized: "U", comment: "Insulin unit abbreviation")
                             )
                             .font(.system(.body, design: .monospaced))
@@ -100,7 +100,7 @@ struct CarbRatioStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 4) {
-                            Text("• A ratio of 10 g/U means 1 unit of insulin covers 10g of carbs")
+                            Text("• A ratio of 10 g/U means 1 unit of insulin covers 10 g of carbs")
                             Text("• A lower number means you need more insulin for the same amount of carbs")
                             Text("• A higher number means you need less insulin for the same amount of carbs")
                             Text("• Different times of day may require different ratios")

+ 4 - 5
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/TherapySettings/InsulinSensitivityStepView.swift

@@ -106,13 +106,12 @@ struct InsulinSensitivityStepView: View {
                             .padding(.horizontal)
 
                         VStack(alignment: .leading, spacing: 4) {
-                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)" +
-                                "\(state.units.rawValue)"
+                            let isfValue = "\(state.units == .mgdL ? Decimal(50) : 50.asMmolL)"
                             Text(
-                                "• An ISF of \(isfValue) means 1 U lowers your glucose by \(isfValue)"
+                                "• An ISF of \(isfValue) \(state.units.rawValue)/U means 1 U lowers your glucose by \(isfValue) \(state.units.rawValue)"
                             )
-                            Text("• A lower number means you're more sensitive to insulin")
-                            Text("• A higher number means you're less sensitive to insulin")
+                            Text("• A lower number means you're less sensitive (more resistant) to insulin")
+                            Text("• A higher number means you're more sensitive (less resistant) to insulin")
                         }
                         .font(.caption)
                         .foregroundColor(.secondary)

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/WelcomeStepView.swift

@@ -15,7 +15,7 @@ struct WelcomeStepView: View {
                     .multilineTextAlignment(.center)
 
                 Text(
-                    "Welcome to Trio - an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
+                    "Welcome to Trio  an automated insulin delivery system for iOS based on the OpenAPS algorithm with adaptations."
                 )
                 .multilineTextAlignment(.leading)
                 .foregroundColor(.secondary)

+ 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)
     }
 }

+ 1 - 1
dexcom-share-client-swift

@@ -1 +1 @@
-Subproject commit 2e9ebf07af058b6286f0e30e2051a62c9fe68a69
+Subproject commit 41cf95dab00f125f7a7602c433aac79fea8fc549