Pārlūkot izejas kodu

Merge branch 'core-data-sync-trio' of github.com:nightscout/Trio-dev into currentTDDForOref

Deniz Cengiz 1 gadu atpakaļ
vecāks
revīzija
01ed59e05d
100 mainītis faili ar 1380 papildinājumiem un 2972 dzēšanām
  1. 2 1
      .github/ISSUE_TEMPLATE/bug-report.md
  2. 2 1
      .github/ISSUE_TEMPLATE/feature-request.md
  3. 12 9
      .github/workflows/build_trio.yml
  4. 89 19
      .github/workflows/create_certs.yml
  5. 4 5
      .github/workflows/validate_secrets.yml
  6. 0 16
      FreeAPS/Sources/APS/OpenAPS/Script.swift
  7. 0 22
      FreeAPS/Sources/Models/GlucoseColorScheme.swift
  8. 0 137
      FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigAppleWatchView.swift
  9. 0 78
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift
  10. 0 213
      FreeAPS/Sources/Services/WatchManager/GarminManager.swift
  11. 0 537
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  12. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/36x36 Circular.png
  13. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
  14. 0 53
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json
  15. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/203x203 Square.png
  16. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
  17. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/84x84 Square.png
  18. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
  19. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/84x84 Circular.png
  20. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
  21. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/40x40 Square.png
  22. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
  23. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/240x240 Square.png
  24. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json
  25. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/300x94.png
  26. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
  27. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/58x58 Square.png
  28. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
  29. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/44x44 Square.png
  30. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
  31. 0 106
      FreeAPSWatch WatchKit Extension/ComplicationController.swift
  32. 0 33
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  33. 0 15
      FreeAPSWatch WatchKit Extension/FreeAPSApp.swift
  34. 0 16
      FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements
  35. 0 22
      FreeAPSWatch WatchKit Extension/Info.plist
  36. 0 25
      FreeAPSWatch WatchKit Extension/NotificationController.swift
  37. 0 13
      FreeAPSWatch WatchKit Extension/NotificationView.swift
  38. 0 20
      FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns
  39. 0 118
      FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift
  40. 0 100
      FreeAPSWatch WatchKit Extension/Views/BolusView.swift
  41. 0 253
      FreeAPSWatch WatchKit Extension/Views/CarbsView.swift
  42. 0 86
      FreeAPSWatch WatchKit Extension/Views/ConfirmationView.swift
  43. 0 444
      FreeAPSWatch WatchKit Extension/Views/MainView.swift
  44. 0 56
      FreeAPSWatch WatchKit Extension/Views/TempTargetsView.swift
  45. 0 201
      FreeAPSWatch WatchKit Extension/WatchStateModel.swift
  46. 0 123
      FreeAPSWatch/Assets.xcassets/AppIcon.appiconset/Contents.json
  47. 1 0
      LiveActivity/LiveActivity.swift
  48. 1 1
      LiveActivity/Views/LiveActivityBGAndTrendView.swift
  49. 1 1
      LiveActivity/Views/LiveActivityChartView.swift
  50. 1 2
      LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift
  51. 14 7
      LiveActivity/Views/LiveActivityView.swift
  52. 32 0
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelLargeView.swift
  53. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift
  54. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  55. 35 0
      LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift
  56. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  57. 4 0
      Model/Classes+Properties/TempTargetRunStored+CoreDataClass.swift
  58. 18 0
      Model/Classes+Properties/TempTargetRunStored+CoreDataProperties.swift
  59. 4 0
      Model/Classes+Properties/TempTargetStored+CoreDataClass.swift
  60. 22 0
      Model/Classes+Properties/TempTargetStored+CoreDataProperties.swift
  61. 19 9
      Model/CoreDataObserver.swift
  62. 1 0
      Model/Helper/CustomNotification.swift
  63. 2 2
      Model/Helper/Determination+helper.swift
  64. 1 1
      Model/Helper/TempTargetRunStored.swift
  65. 4 4
      TempTargetRunStored+CoreDataProperties.swift
  66. 6 6
      TempTargetStored+CoreDataProperties.swift
  67. 0 0
      Trio Watch App Extension/Assets.xcassets/AccentColor.colorset/Contents.json
  68. 14 0
      Trio Watch App Extension/Assets.xcassets/AppIcon.appiconset/Contents.json
  69. 0 0
      Trio Watch App Extension/Assets.xcassets/AppIcon.appiconset/trioBlack watch.png
  70. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  71. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  72. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json
  73. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Chart.colorset/Contents.json
  74. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Contents.json
  75. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  76. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  77. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  78. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  79. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  80. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopYellow.colorset/Contents.json
  81. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  82. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/TempBasal.colorset/Contents.json
  83. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json
  84. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json
  85. 0 0
      Trio Watch App Extension/Assets.xcassets/Contents.json
  86. 64 0
      Trio Watch App Extension/Helper/Helper+ButtonStyles.swift
  87. 56 0
      Trio Watch App Extension/Helper/Helper+Enums.swift
  88. 32 0
      Trio Watch App Extension/Helper/Helper+Extensions.swift
  89. 0 0
      Trio Watch App Extension/Preview Content/Preview Assets.xcassets/Contents.json
  90. 9 0
      Trio Watch App Extension/TrioWatchApp.swift
  91. 56 0
      Trio Watch App Extension/Views/AcknowledgementPendingView.swift
  92. 123 0
      Trio Watch App Extension/Views/BolusConfirmationView.swift
  93. 164 0
      Trio Watch App Extension/Views/BolusInputView.swift
  94. 63 0
      Trio Watch App Extension/Views/BolusProgressOverlay.swift
  95. 118 0
      Trio Watch App Extension/Views/CarbsInputView.swift
  96. 90 0
      Trio Watch App Extension/Views/GlucoseChartView.swift
  97. 159 0
      Trio Watch App Extension/Views/GlucoseTrendView.swift
  98. 77 0
      Trio Watch App Extension/Views/OverridePresetsView.swift
  99. 77 0
      Trio Watch App Extension/Views/TempTargetPresetsView.swift
  100. 0 0
      Trio Watch App Extension/Views/TreatmentMenuView.swift

+ 2 - 1
.github/ISSUE_TEMPLATE/bug-report.md

@@ -2,7 +2,8 @@
 name: "\U0001F41B Bug report"
 about: Create a report to help us fix things
 title: ''
-labels: ['bug', 'needs-triage']
+labels: ['needs-triage']
+type: "bug"
 assignees: ''
 projects: ['nightscout/2']
 

+ 2 - 1
.github/ISSUE_TEMPLATE/feature-request.md

@@ -2,7 +2,8 @@
 name: "\U0001F4A1 Feature request \U0001F4A1"
 about: Suggest an idea for this project
 title: ''
-labels: ['enhancement', 'needs-triage']
+labels: ['needs-triage']
+types: "feature"
 assignees: ''
 projects: ['nightscout/2']
 

+ 12 - 9
.github/workflows/build_trio.yml

@@ -10,7 +10,7 @@ on:
     - cron: "0 8 * * 3" # Checks for updates at 08:00 UTC every Wednesday
     - cron: "0 6 1 * *" # Builds the app on the 1st of every month at 06:00 UTC
 
-env:  
+env:
   UPSTREAM_REPO: nightscout/Trio
   UPSTREAM_BRANCH: ${{ github.ref_name }} # branch on upstream repository to sync from (replace with specific branch name if needed)
   TARGET_BRANCH: ${{ github.ref_name }} # target branch on fork to be kept in sync, and target branch on upstream to be kept alive (replace with specific branch name if needed)
@@ -18,15 +18,17 @@ env:
   ALIVE_BRANCH_DEV: alive-dev
 
 jobs:
-  validate:
-    name: Validate
-    uses: ./.github/workflows/validate_secrets.yml
+  # Checks if Distribution certificate is present and valid, optionally nukes and
+  # creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
+  check_certs:
+    name: Check certificates
+    uses: ./.github/workflows/create_certs.yml
     secrets: inherit
 
   # Checks if GH_PAT holds workflow permissions
   # Checks for existence of alive branch; if non-existent creates it
   check_alive_and_permissions:
-    needs: validate
+    needs: check_certs
     runs-on: ubuntu-latest
     name: Check alive branch and permissions
     permissions:
@@ -96,7 +98,7 @@ jobs:
   # Checks for changes in upstream repository; if changes exist prompts sync for build
   # Performs keepalive to avoid stale fork
   check_latest_from_upstream:
-    needs: [validate, check_alive_and_permissions]
+    needs: [check_certs, check_alive_and_permissions]
     runs-on: ubuntu-latest
     name: Check upstream and keep alive
     outputs:
@@ -181,11 +183,12 @@ jobs:
           echo "Synchronizing your fork of <code>Trio</code> with the upstream repository <code>nightscout/Trio</code> will be skipped." >> $GITHUB_STEP_SUMMARY
           echo "If you want to enable automatic builds and updates for your Trio, please follow the instructions \
               under the following path <code>Trio/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
-  
+
   # Builds Trio
   build:
     name: Build
-    needs: [validate, check_alive_and_permissions, check_latest_from_upstream]
+    needs:
+      [check_certs, check_alive_and_permissions, check_latest_from_upstream]
     runs-on: macos-15
     permissions:
       contents: write
@@ -199,7 +202,7 @@ jobs:
     steps:
       - name: Select Xcode version
         run: "sudo xcode-select --switch /Applications/Xcode_16.0.app/Contents/Developer"
-      
+
       - name: Checkout Repo for syncing
         if: |
           needs.check_alive_and_permissions.outputs.WORKFLOW_PERMISSION == 'true' &&

+ 89 - 19
.github/workflows/create_certs.yml

@@ -1,7 +1,16 @@
 name: 3. Create Certificates
 run-name: Create Certificates (${{ github.ref_name }})
-on:
-  workflow_dispatch:
+
+on: [workflow_call, workflow_dispatch]
+
+env:
+  TEAMID: ${{ secrets.TEAMID }}
+  GH_PAT: ${{ secrets.GH_PAT }}
+  GH_TOKEN: ${{ secrets.GH_PAT }}
+  MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+  FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
+  FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
+  FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
 
 jobs:
   validate:
@@ -9,12 +18,14 @@ jobs:
     uses: ./.github/workflows/validate_secrets.yml
     secrets: inherit
 
-  certificates:
-    name: Create Certificates
+  create_certs:
+    name: Certificates
     needs: validate
     runs-on: macos-15
-    steps:
+    outputs:
+      new_certificate_needed: ${{ steps.set_output.outputs.new_certificate_needed }}
 
+    steps:
       # Checks-out the repo
       - name: Checkout Repo
         uses: actions/checkout@v4
@@ -34,17 +45,76 @@ jobs:
       - name: Install Project Dependencies
         run: bundle install
 
-      # Sync the GitHub runner clock with the Windows time server (workaround as suggested in https://github.com/actions/runner/issues/2996)
-      - name: Sync clock
-        run: sudo sntp -sS time.windows.com
-
-      # Create or update certificates for app
-      - name: Create Certificates
-        run: bundle exec fastlane certs
-        env:
-          TEAMID: ${{ secrets.TEAMID }}
-          GH_PAT: ${{ secrets.GH_PAT }}
-          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
-          FASTLANE_KEY_ID: ${{ secrets.FASTLANE_KEY_ID }}
-          FASTLANE_ISSUER_ID: ${{ secrets.FASTLANE_ISSUER_ID }}
-          FASTLANE_KEY: ${{ secrets.FASTLANE_KEY }}
+      # Create or update Distribution certificate and provisioning profiles
+      - name: Check and create or update Distribution certificate and profiles if needed
+        run: |
+          echo "Running Fastlane certs lane..."
+          bundle exec fastlane certs || true # ignore and continue on errors without annotating an exit code
+
+      - name: Check Distribution certificate and launch Nuke certificates if needed
+        run: bundle exec fastlane check_and_renew_certificates
+        id: check_certs
+
+      - name: Set output and annotations based on Fastlane result
+        id: set_output
+        run: |
+          CERT_STATUS_FILE="${{ github.workspace }}/fastlane/new_certificate_needed.txt"
+          ENABLE_NUKE_CERTS=${{ vars.ENABLE_NUKE_CERTS }}
+
+          if [ -f "$CERT_STATUS_FILE" ]; then
+            CERT_STATUS=$(cat "$CERT_STATUS_FILE" | tr -d '\n' | tr -d '\r') # Read file content and strip newlines
+            echo "new_certificate_needed: $CERT_STATUS"
+            echo "new_certificate_needed=$CERT_STATUS" >> $GITHUB_OUTPUT
+          else
+            echo "Certificate status file not found. Defaulting to false."
+            echo "new_certificate_needed=false" >> $GITHUB_OUTPUT
+          fi
+
+          # Check if ENABLE_NUKE_CERTS is not set to true when certs are valid
+          if [ "$CERT_STATUS" != "true" ] && [ "$ENABLE_NUKE_CERTS" != "true" ]; then
+            echo "::notice::🔔 Automated renewal of certificates is disabled because the repository variable ENABLE_NUKE_CERTS is not set to 'true'."
+          fi
+
+          # Check if ENABLE_NUKE_CERTS is not set to true when certs are not valid
+          if [ "$CERT_STATUS" = "true" ] && [ "$ENABLE_NUKE_CERTS" != "true" ]; then
+            echo "::error::❌ No valid distribution certificate found. Automated renewal of certificates was skipped because the repository variable ENABLE_NUKE_CERTS is not set to 'true'."
+            exit 1
+          fi
+
+          # Check if vars.FORCE_NUKE_CERTS is not set to true
+          if [ vars.FORCE_NUKE_CERTS = "true" ]; then
+            echo "::warning::‼️ Nuking of certificates was forced because the repository variable FORCE_NUKE_CERTS is set to 'true'."
+          fi
+
+  # Nuke Certs if needed, and if the repository variable ENABLE_NUKE_CERTS is set to 'true', or if FORCE_NUKE_CERTS is set to 'true', which will always force certs to be nuked
+  nuke_certs:
+    name: Nuke certificates
+    needs: [validate, create_certs]
+    runs-on: macos-14
+    if: ${{ (needs.create_certs.outputs.new_certificate_needed == 'true' && vars.ENABLE_NUKE_CERTS == 'true') || vars.FORCE_NUKE_CERTS == 'true' }}
+    steps:
+      - name: Output from step id 'check_certs'
+        run: echo "new_certificate_needed=${{ needs.create_certs.outputs.new_certificate_needed }}"
+
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Install dependencies
+        run: bundle install
+
+      - name: Run Fastlane nuke_certs
+        run: |
+          set -e # Set error immediately after this step if error occurs
+          bundle exec fastlane nuke_certs
+
+      - name: Recreate Distribution certificate after nuking
+        run: |
+          set -e # Set error immediately after this step if error occurs
+          bundle exec fastlane certs
+
+      - name: Add success annotations for nuke and certificate recreation
+        if: ${{ success() }}
+        run: |
+          echo "::warning::⚠️ All Distribution certificates and TestFlight profiles have been revoked and recreated."
+          echo "::warning::❗️ If you have other apps being distributed by GitHub Actions / Fastlane / TestFlight that does not renew certificates automatically, please run the '3. Create Certificates' workflow for each of these apps to allow these apps to be built."
+          echo "::warning::✅ But don't worry about your existing TestFlight builds, they will keep working!"

+ 4 - 5
.github/workflows/validate_secrets.yml

@@ -178,16 +178,15 @@ jobs:
           elif ! echo "$FASTLANE_KEY" | openssl pkcs8 -nocrypt >/dev/null; then
             failed=true
             echo "::error::The FASTLANE_KEY secret is set but invalid. Verify that you copied it correctly from the API Key file (*.p8) you downloaded and try again."
-          elif ! bundle exec fastlane validate_secrets 2>&1 | tee fastlane.log; then
+          elif ! (bundle exec fastlane validate_secrets 2>&1 || true) | tee fastlane.log; then # ignore "fastlane validate_secrets" errors and continue on errors without annotating an exit code
             if grep -q "bad decrypt" fastlane.log; then
               failed=true
               echo "::error::Unable to decrypt the Match-Secrets repository using the MATCH_PASSWORD secret. Verify that it is set correctly and try again."
             elif grep -q -e "required agreement" -e "license agreement" fastlane.log; then
               failed=true
-              echo "::error::Unable to create a valid authorization token for the App Store Connect API. Verify that the latest developer program license agreement has been accepted at https://developer.apple.com/account (review and accept any updated agreement), then wait a few minutes for changes to propagate and try again."
-            elif ! grep -q -e "No code signing identity found" -e "Could not install WWDR certificate" fastlane.log; then
-              failed=true
-              echo "::error::Unable to create a valid authorization token for the App Store Connect API. Verify that the FASTLANE_ISSUER_ID, FASTLANE_KEY_ID, and FASTLANE_KEY secrets are set correctly and try again."
+              echo "::error::❗️ Verify that the latest developer program license agreement has been accepted at https://developer.apple.com/account (review and accept any updated agreement), then wait a few minutes for changes to take effect and try again."
+            elif grep -q "Your certificate .* is not valid" fastlane.log; then
+              echo "::notice::Your Distribution certificate is invalid or expired. Automated renewal of the certificate will be attempted."
             fi
           fi
 

+ 0 - 16
FreeAPS/Sources/APS/OpenAPS/Script.swift

@@ -1,16 +0,0 @@
-import Foundation
-
-struct Script {
-    let name: String
-    let body: String
-
-    init(name: String) {
-        self.name = name
-        body = try! String(contentsOf: Bundle.main.url(forResource: "javascript/\(name)", withExtension: "")!)
-    }
-
-    init(name: String, body: String) {
-        self.name = name
-        self.body = body
-    }
-}

+ 0 - 22
FreeAPS/Sources/Models/GlucoseColorScheme.swift

@@ -1,22 +0,0 @@
-//
-//  GlucoseColorScheme.swift
-//  FreeAPS
-//
-//  Created by Cengiz Deniz on 27.09.24.
-//
-import Foundation
-
-public enum GlucoseColorScheme: String, JSON, CaseIterable, Identifiable, Codable, Hashable {
-    public var id: String { rawValue }
-    case staticColor
-    case dynamicColor
-
-    var displayName: String {
-        switch self {
-        case .staticColor:
-            return "Static"
-        case .dynamicColor:
-            return "Dynamic"
-        }
-    }
-}

+ 0 - 137
FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigAppleWatchView.swift

@@ -1,137 +0,0 @@
-import SwiftUI
-import Swinject
-
-struct WatchConfigAppleWatchView: BaseView {
-    let resolver: Resolver
-    @ObservedObject var state: WatchConfig.StateModel
-
-    @State private var shouldDisplayHint: Bool = false
-    @State var hintDetent = PresentationDetent.large
-    @State var selectedVerboseHint: AnyView?
-    @State var hintLabel: String?
-    @State private var decimalPlaceholder: Decimal = 0.0
-    @State private var booleanPlaceholder: Bool = false
-
-    @Environment(\.colorScheme) var colorScheme
-    @Environment(AppState.self) var appState
-
-    private func onDelete(offsets: IndexSet) {
-        state.devices.remove(atOffsets: offsets)
-        state.deleteGarminDevice()
-    }
-
-    var body: some View {
-        List {
-            Section(
-                header: Text("Apple Watch Configuration"),
-                content: {
-                    VStack {
-                        Picker(
-                            selection: $state.selectedAwConfig,
-                            label: Text("Display on Watch")
-                        ) {
-                            ForEach(AwConfig.allCases) { selection in
-                                Text(selection.displayName).tag(selection)
-                            }
-                        }.padding(.top)
-
-                        HStack(alignment: .center) {
-                            Text(
-                                "Select the information to display."
-                            )
-                            .font(.footnote)
-                            .foregroundColor(.secondary)
-                            .lineLimit(nil)
-                            Spacer()
-                            Button(
-                                action: {
-                                    hintLabel = "Display on Watch"
-                                    selectedVerboseHint =
-                                        AnyView(VStack(alignment: .leading, spacing: 5) {
-                                            Text("Choose between the following:")
-                                            Text("• Heart Rate")
-                                            Text("• Glucose Target")
-                                            Text("• Steps")
-                                            Text("• ISF")
-                                            Text("• % Override")
-                                        })
-                                    shouldDisplayHint.toggle()
-                                },
-                                label: {
-                                    HStack {
-                                        Image(systemName: "questionmark.circle")
-                                    }
-                                }
-                            ).buttonStyle(BorderlessButtonStyle())
-                        }.padding(.top)
-                    }.padding(.bottom)
-                }
-            ).listRowBackground(Color.chart)
-
-            SettingInputSection(
-                decimalValue: $decimalPlaceholder,
-                booleanValue: $state.displayFatAndProteinOnWatch,
-                shouldDisplayHint: $shouldDisplayHint,
-                selectedVerboseHint: Binding(
-                    get: { selectedVerboseHint },
-                    set: {
-                        selectedVerboseHint = $0.map { AnyView($0) }
-                        hintLabel = "Show Protein and Fat"
-                    }
-                ),
-                units: state.units,
-                type: .boolean,
-                label: "Show Protein and Fat",
-                miniHint: "Allow protein and fat entries on watch.",
-                verboseHint: Text("When enabled, protein and fat will show in the carb entry screen of the Apple Watch.")
-            )
-
-            SettingInputSection(
-                decimalValue: $decimalPlaceholder,
-                booleanValue: $state.confirmBolusFaster,
-                shouldDisplayHint: $shouldDisplayHint,
-                selectedVerboseHint: Binding(
-                    get: { selectedVerboseHint },
-                    set: {
-                        selectedVerboseHint = $0.map { AnyView($0) }
-                        hintLabel = "Confirm Bolus Faster"
-                    }
-                ),
-                units: state.units,
-                type: .boolean,
-                label: "Confirm Bolus Faster",
-                miniHint: "Reduce the number of crown rotations required for bolus confirmation.",
-                verboseHint: Text(
-                    "Enabling this feature lowers the number of turns on the crown dial required when confirming a bolus."
-                )
-            )
-
-            Section(
-                header: Text("Contact Image"),
-                content: {
-                    VStack {
-                        HStack {
-                            NavigationLink("Contacts Configuration") {
-                                ContactImage.RootView(resolver: resolver)
-                            }.foregroundStyle(Color.accentColor)
-                        }
-                    }
-                }
-            ).listRowBackground(Color.chart)
-        }
-        .listSectionSpacing(sectionSpacing)
-        .sheet(isPresented: $shouldDisplayHint) {
-            SettingInputHintView(
-                hintDetent: $hintDetent,
-                shouldDisplayHint: $shouldDisplayHint,
-                hintLabel: hintLabel ?? "",
-                hintText: selectedVerboseHint ?? AnyView(EmptyView()),
-                sheetTitle: "Help"
-            )
-        }
-        .navigationTitle("Apple Watch")
-        .navigationBarTitleDisplayMode(.automatic)
-        .scrollContentBackground(.hidden)
-        .background(appState.trioBackgroundColor(for: colorScheme))
-    }
-}

+ 0 - 78
FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift

@@ -1,78 +0,0 @@
-import ConnectIQ
-import SwiftUI
-
-enum AwConfig: String, JSON, CaseIterable, Identifiable, Codable {
-    var id: String { rawValue }
-    case HR
-    case BGTarget
-    case steps
-    case isf
-    case override
-
-    var displayName: String {
-        switch self {
-        case .BGTarget:
-            return NSLocalizedString("Glucose Target", comment: "")
-        case .HR:
-            return NSLocalizedString("Heart Rate", comment: "")
-        case .steps:
-            return NSLocalizedString("Steps", comment: "")
-        case .isf:
-            return NSLocalizedString("ISF", comment: "")
-        case .override:
-            return NSLocalizedString("% Override", comment: "")
-        }
-    }
-}
-
-extension WatchConfig {
-    final class StateModel: BaseStateModel<Provider> {
-        @Injected() private var garmin: GarminManager!
-
-        @Published var units: GlucoseUnits = .mgdL
-        @Published var devices: [IQDevice] = []
-        @Published var selectedAwConfig: AwConfig = .HR
-        @Published var displayFatAndProteinOnWatch = false
-        @Published var confirmBolusFaster = false
-
-        private(set) var preferences = Preferences()
-
-        override func subscribe() {
-            preferences = provider.preferences
-
-            units = settingsManager.settings.units
-
-            subscribeSetting(\.displayFatAndProteinOnWatch, on: $displayFatAndProteinOnWatch) { displayFatAndProteinOnWatch = $0 }
-            subscribeSetting(\.confirmBolusFaster, on: $confirmBolusFaster) { confirmBolusFaster = $0 }
-            subscribeSetting(\.displayOnWatch, on: $selectedAwConfig) { selectedAwConfig = $0 }
-            didSet: { [weak self] value in
-                // for compatibility with old displayHR
-                switch value {
-                case .HR:
-                    self?.settingsManager.settings.displayHR = true
-                default:
-                    self?.settingsManager.settings.displayHR = false
-                }
-            }
-
-            devices = garmin.devices
-        }
-
-        func selectGarminDevices() {
-            garmin.selectDevices()
-                .receive(on: DispatchQueue.main)
-                .weakAssign(to: \.devices, on: self)
-                .store(in: &lifetime)
-        }
-
-        func deleteGarminDevice() {
-            garmin.updateListDevices(devices: devices)
-        }
-    }
-}
-
-extension WatchConfig.StateModel: SettingsObserver {
-    func settingsDidChange(_: FreeAPSSettings) {
-        units = settingsManager.settings.units
-    }
-}

+ 0 - 213
FreeAPS/Sources/Services/WatchManager/GarminManager.swift

@@ -1,213 +0,0 @@
-import Combine
-import ConnectIQ
-import Foundation
-import Swinject
-
-protocol GarminManager {
-    func selectDevices() -> AnyPublisher<[IQDevice], Never>
-    func updateListDevices(devices: [IQDevice])
-    var devices: [IQDevice] { get }
-    func sendState(_ data: Data)
-    var stateRequet: (() -> (Data))? { get set }
-}
-
-extension Notification.Name {
-    static let openFromGarminConnect = Notification.Name("Notification.Name.openFromGarminConnect")
-}
-
-final class BaseGarminManager: NSObject, GarminManager, Injectable {
-    private enum Config {
-        static let watchfaceUUID = UUID(uuidString: "EC3420F6-027D-49B3-B45F-D81D6D3ED90A")
-        static let watchdataUUID = UUID(uuidString: "71CF0982-CA41-42A5-8441-EA81D36056C3")
-    }
-
-    private let connectIQ = ConnectIQ.sharedInstance()
-
-    private let router = FreeAPSApp.resolver.resolve(Router.self)!
-
-    @Injected() private var notificationCenter: NotificationCenter!
-
-    @Persisted(key: "BaseGarminManager.persistedDevices") private var persistedDevices: [CodableDevice] = []
-
-    private var watchfaces: [IQApp] = []
-
-    var stateRequet: (() -> (Data))?
-
-    private let stateSubject = PassthroughSubject<NSDictionary, Never>()
-
-    private(set) var devices: [IQDevice] = [] {
-        didSet {
-            persistedDevices = devices.map(CodableDevice.init)
-            watchfaces = []
-            devices.forEach { device in
-                connectIQ?.register(forDeviceEvents: device, delegate: self)
-                let watchfaceApp = IQApp(
-                    uuid: Config.watchfaceUUID,
-                    store: UUID(),
-                    device: device
-                )
-                let watchDataFieldApp = IQApp(
-                    uuid: Config.watchdataUUID,
-                    store: UUID(),
-                    device: device
-                )
-                watchfaces.append(watchfaceApp!)
-                watchfaces.append(watchDataFieldApp!)
-                connectIQ?.register(forAppMessages: watchfaceApp, delegate: self)
-            }
-        }
-    }
-
-    private var lifetime = Lifetime()
-    private var selectPromise: Future<[IQDevice], Never>.Promise?
-
-    init(resolver: Resolver) {
-        super.init()
-        connectIQ?.initialize(withUrlScheme: "Trio", uiOverrideDelegate: self)
-        injectServices(resolver)
-        restoreDevices()
-        subscribeToOpenFromGarminConnect()
-        setupApplications()
-        subscribeState()
-    }
-
-    private func subscribeToOpenFromGarminConnect() {
-        notificationCenter
-            .publisher(for: .openFromGarminConnect)
-            .sink { notification in
-                guard let url = notification.object as? URL else { return }
-                self.parseDevicesFor(url: url)
-            }
-            .store(in: &lifetime)
-    }
-
-    private func subscribeState() {
-        func sendToWatchface(state: NSDictionary) {
-            watchfaces.forEach { app in
-                connectIQ?.getAppStatus(app) { status in
-                    guard status?.isInstalled ?? false else {
-                        debug(.service, "Garmin: watchface app not installed")
-                        return
-                    }
-                    debug(.service, "Garmin: sending message to watchface")
-                    self.sendMessage(state, to: app)
-                }
-            }
-        }
-
-        stateSubject
-            .throttle(for: .seconds(10), scheduler: DispatchQueue.main, latest: true)
-            .sink { state in
-                sendToWatchface(state: state)
-            }
-            .store(in: &lifetime)
-    }
-
-    private func restoreDevices() {
-        devices = persistedDevices.map(\.iqDevice)
-    }
-
-    private func parseDevicesFor(url: URL) {
-        devices = connectIQ?.parseDeviceSelectionResponse(from: url) as? [IQDevice] ?? []
-        selectPromise?(.success(devices))
-        selectPromise = nil
-    }
-
-    private func setupApplications() {
-        devices.forEach { _ in
-        }
-    }
-
-    func selectDevices() -> AnyPublisher<[IQDevice], Never> {
-        Future { promise in
-            self.selectPromise = promise
-            self.connectIQ?.showDeviceSelection()
-        }
-        .timeout(120, scheduler: DispatchQueue.main)
-        .replaceEmpty(with: [])
-        .eraseToAnyPublisher()
-    }
-
-    func updateListDevices(devices: [IQDevice]) {
-        self.devices = devices
-    }
-
-    func sendState(_ data: Data) {
-        guard let object = try? JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary else {
-            return
-        }
-        stateSubject.send(object)
-    }
-
-    private func sendMessage(_ msg: NSDictionary, to app: IQApp) {
-        connectIQ?.sendMessage(msg, to: app, progress: { _, _ in
-            // debug(.service, "Garmin: sending progress: \(Int(Double(sent) / Double(all) * 100)) %")
-        }, completion: { result in
-            if result == .success {
-                debug(.service, "Garmin: message sent")
-            } else {
-                debug(.service, "Garmin: message failed")
-            }
-        })
-    }
-}
-
-extension BaseGarminManager: IQUIOverrideDelegate {
-    func needsToInstallConnectMobile() {
-        debug(.apsManager, NSLocalizedString("Garmin is not available", comment: ""))
-        let messageCont = MessageContent(
-            content: NSLocalizedString(
-                "The app Garmin Connect must be installed to use for Trio.\n Go to App Store to download it",
-                comment: ""
-            ),
-            type: .warning,
-            subtype: .misc,
-            title: NSLocalizedString("Garmin is not available", comment: "")
-        )
-        router.alertMessage.send(messageCont)
-    }
-}
-
-extension BaseGarminManager: IQDeviceEventDelegate {
-    func deviceStatusChanged(_ device: IQDevice, status: IQDeviceStatus) {
-        switch status {
-        case .invalidDevice:
-            debug(.service, "Garmin: invalidDevice, Device: \(device.uuid!)")
-        case .bluetoothNotReady:
-            debug(.service, "Garmin: bluetoothNotReady, Device: \(device.uuid!)")
-        case .notFound:
-            debug(.service, "Garmin: notFound, Device: \(device.uuid!)")
-        case .notConnected:
-            debug(.service, "Garmin: notConnected, Device: \(device.uuid!)")
-        case .connected:
-            debug(.service, "Garmin: connected, Device: \(device.uuid!)")
-        @unknown default:
-            debug(.service, "Garmin: unknown state, Device: \(device.uuid!)")
-        }
-    }
-}
-
-extension BaseGarminManager: IQAppMessageDelegate {
-    func receivedMessage(_ message: Any, from app: IQApp) {
-        print("ASDF: got message: \(message) from app: \(app.uuid!)")
-        if let status = message as? String, status == "status", let watchState = stateRequet?() {
-            sendState(watchState)
-        }
-    }
-}
-
-struct CodableDevice: Codable, Equatable {
-    let id: UUID
-    let modelName: String
-    let friendlyName: String
-
-    init(iqDevice: IQDevice) {
-        id = iqDevice.uuid
-        modelName = iqDevice.modelName
-        friendlyName = iqDevice.modelName
-    }
-
-    var iqDevice: IQDevice {
-        IQDevice(id: id, modelName: modelName, friendlyName: friendlyName)
-    }
-}

+ 0 - 537
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -1,537 +0,0 @@
-import Combine
-import CoreData
-import Foundation
-import Swinject
-import WatchConnectivity
-
-protocol WatchManager {}
-
-final class BaseWatchManager: NSObject, WatchManager, Injectable {
-    private let session: WCSession
-    private var state = WatchState()
-    private let processQueue = DispatchQueue(label: "BaseWatchManager.processQueue")
-
-    @Injected() private var broadcaster: Broadcaster!
-    @Injected() private var settingsManager: SettingsManager!
-    @Injected() private var apsManager: APSManager!
-    @Injected() private var storage: FileStorage!
-    @Injected() private var carbsStorage: CarbsStorage!
-    @Injected() private var tempTargetsStorage: TempTargetsStorage!
-    @Injected() private var garmin: GarminManager!
-    @Injected() private var glucoseStorage: GlucoseStorage!
-
-    private var glucoseFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 0
-        if settingsManager.settings.units == .mmolL {
-            formatter.minimumFractionDigits = 1
-            formatter.maximumFractionDigits = 1
-        }
-        formatter.roundingMode = .halfUp
-        return formatter
-    }
-
-    private var eventualFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        return formatter
-    }
-
-    private var deltaFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = settingsManager.settings.units == .mmolL ? 1 : 0
-        formatter.positivePrefix = "+"
-        formatter.negativePrefix = "-"
-        return formatter
-    }
-
-    private var targetFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.maximumFractionDigits = 1
-        return formatter
-    }
-
-    let context = CoreDataStack.shared.newTaskContext()
-    let viewContext = CoreDataStack.shared.persistentContainer.viewContext
-
-    private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
-    private var subscriptions = Set<AnyCancellable>()
-
-    private var lifetime = Lifetime()
-
-    init(resolver: Resolver, session: WCSession = .default) {
-        self.session = session
-        super.init()
-        injectServices(resolver)
-        registerHandlers()
-        registerSubscribers()
-
-        coreDataPublisher =
-            changedObjectsOnManagedObjectContextDidSavePublisher()
-                .receive(on: DispatchQueue.global(qos: .background))
-                .share()
-                .eraseToAnyPublisher()
-
-        Task {
-            await configureState()
-        }
-
-        if WCSession.isSupported() {
-            session.delegate = self
-            session.activate()
-        }
-
-        broadcaster.register(SettingsObserver.self, observer: self)
-        broadcaster.register(PumpHistoryObserver.self, observer: self)
-        broadcaster.register(PumpSettingsObserver.self, observer: self)
-        broadcaster.register(BasalProfileObserver.self, observer: self)
-        broadcaster.register(TempTargetsObserver.self, observer: self)
-        broadcaster.register(CarbsObserver.self, observer: self)
-        broadcaster.register(PumpBatteryObserver.self, observer: self)
-        broadcaster.register(PumpReservoirObserver.self, observer: self)
-        garmin.stateRequet = { [weak self] () -> Data in
-            guard let self = self, let data = try? JSONEncoder().encode(self.state) else {
-                warning(.service, "Cannot encode watch state")
-                return Data()
-            }
-            return data
-        }
-    }
-
-    private func registerSubscribers() {
-        glucoseStorage.updatePublisher
-            .receive(on: DispatchQueue.global(qos: .background))
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                Task {
-                    await self.configureState()
-                }
-            }
-            .store(in: &subscriptions)
-    }
-
-    private func registerHandlers() {
-        coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task {
-                await self.configureState()
-            }
-        }.store(in: &subscriptions)
-
-        coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task {
-                await self.configureState()
-            }
-        }.store(in: &subscriptions)
-
-        // Observes Deletion of Glucose Objects
-        coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            Task {
-                await self.configureState()
-            }
-        }.store(in: &subscriptions)
-    }
-
-    private func fetchlastDetermination() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OrefDetermination.self,
-            onContext: context,
-            predicate: NSPredicate.enactedDetermination,
-            key: "timestamp",
-            ascending: false,
-            fetchLimit: 1
-        )
-
-        return await context.perform {
-            guard let fetchedResults = results as? [OrefDetermination] else { return [] }
-
-            return fetchedResults.map(\.objectID)
-        }
-    }
-
-    private func fetchLatestOverride() async -> NSManagedObjectID? {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: OverrideStored.self,
-            onContext: context,
-            predicate: NSPredicate.predicateForOneDayAgo,
-            key: "date",
-            ascending: false,
-            fetchLimit: 1,
-            propertiesToFetch: ["enabled", "percentage", "objectID"]
-        )
-
-        return await context.perform {
-            guard let fetchedResults = results as? [[String: Any]] else { return nil }
-
-            return fetchedResults.compactMap { $0["objectID"] as? NSManagedObjectID }.first
-        }
-    }
-
-    private func fetchGlucose() async -> [NSManagedObjectID] {
-        let results = await CoreDataStack.shared.fetchEntitiesAsync(
-            ofType: GlucoseStored.self,
-            onContext: context,
-            predicate: NSPredicate.predicateFor120MinAgo,
-            key: "date",
-            ascending: false,
-            fetchLimit: 24,
-            batchSize: 12
-        )
-
-        return await context.perform {
-            guard let glucoseResults = results as? [GlucoseStored] else {
-                return []
-            }
-
-            return glucoseResults.map(\.objectID)
-        }
-    }
-
-    @MainActor private func configureState() async {
-        let glucoseValuesIds = await fetchGlucose()
-        async let getLatestDeterminationIds = fetchlastDetermination()
-        async let getlatestOverrideId = fetchLatestOverride()
-
-        let latestOverrideId = await getlatestOverrideId
-
-        guard let lastDeterminationId = await getLatestDeterminationIds.first else {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to get last Determination")
-            return
-        }
-
-        do {
-            let glucoseValues: [GlucoseStored] = await CoreDataStack.shared
-                .getNSManagedObject(with: glucoseValuesIds, context: viewContext)
-            let lastDetermination = try viewContext.existingObject(with: lastDeterminationId) as? OrefDetermination
-            let recommendedInsulin = await newBolusCalc(
-                glucoseIds: glucoseValuesIds,
-                determinationId: lastDeterminationId
-            )
-
-            var latestOverride: OverrideStored?
-            if let id = latestOverrideId {
-                latestOverride = try viewContext.existingObject(with: id) as? OverrideStored
-            }
-
-            await MainActor.run { [weak self] in
-                guard let self = self else { return }
-
-                if let firstGlucoseValue = glucoseValues.first {
-                    let value = self.settingsManager.settings.units == .mgdL
-                        ? Decimal(firstGlucoseValue.glucose)
-                        : Decimal(firstGlucoseValue.glucose).asMmolL
-
-                    self.state.glucose = self.glucoseFormatter.string(from: value as NSNumber)
-                    self.state.trend = firstGlucoseValue.directionEnum?.symbol
-
-                    let delta = glucoseValues.count >= 2
-                        ? Decimal(firstGlucoseValue.glucose) - Decimal(glucoseValues.dropFirst().first?.glucose ?? 0)
-                        : 0
-                    let deltaConverted = self.settingsManager.settings.units == .mgdL ? delta : delta.asMmolL
-                    self.state.delta = self.deltaFormatter.string(from: deltaConverted as NSNumber)
-                    self.state.trendRaw = firstGlucoseValue.direction
-                    self.state.glucoseDate = firstGlucoseValue.date
-                }
-
-                self.state.lastLoopDate = lastDetermination?.timestamp
-                self.state.lastLoopDateInterval = self.state.lastLoopDate.map {
-                    guard $0.timeIntervalSince1970 > 0 else { return 0 }
-                    return UInt64($0.timeIntervalSince1970)
-                }
-                self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement
-                self.state.maxCOB = self.settingsManager.preferences.maxCOB
-                self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
-                self.state.carbsRequired = lastDetermination?.carbsRequired as? Decimal
-                self.state.bolusRecommended = self.apsManager
-                    .roundBolus(amount: max(recommendedInsulin, 0))
-                self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
-                self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
-                self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
-
-                self.state.iob = lastDetermination?.iob as? Decimal
-                if let cobValue = lastDetermination?.cob {
-                    self.state.cob = Decimal(cobValue)
-                } else {
-                    self.state.cob = 0
-                }
-                self.state.tempTargets = self.tempTargetsStorage.presets()
-                    .map { target -> TempTargetWatchPreset in
-                        let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in
-                            guard currentTarget.id == target.id else { return nil }
-                            let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60))
-                            return date > Date() ? date : nil
-                        }
-                        return TempTargetWatchPreset(
-                            name: target.displayName,
-                            id: target.id,
-                            description: self.descriptionForTarget(target),
-                            until: untilDate
-                        )
-                    }
-                self.state.displayOnWatch = self.settingsManager.settings.displayOnWatch
-                self.state.displayFatAndProteinOnWatch = self.settingsManager.settings.displayFatAndProteinOnWatch
-                self.state.confirmBolusFaster = self.settingsManager.settings.confirmBolusFaster
-
-                if let eventualBG = self.settingsManager.settings.units == .mgdL ? lastDetermination?
-                    .eventualBG : lastDetermination?
-                    .eventualBG?.decimalValue.asMmolL as NSDecimalNumber?
-                {
-                    let eventualBGAsString = self.eventualFormatter.string(from: eventualBG)
-                    self.state.eventualBG = eventualBGAsString.map { "⇢ " + $0 }
-                    self.state.eventualBGRaw = eventualBGAsString
-                }
-
-                self.state.isf = lastDetermination?.insulinSensitivity as? Decimal
-
-                if let latestOverride = latestOverride {
-                    if latestOverride.enabled {
-                        let percentString = "\(latestOverride.percentage.formatted(.number)) %"
-                        self.state.override = percentString
-                    } else {
-                        self.state.override = "100 %"
-                    }
-                }
-
-                self.sendState()
-            }
-
-        } catch let error as NSError {
-            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to configure state with error: \(error)")
-        }
-    }
-
-    private func sendState() {
-        guard let data = try? JSONEncoder().encode(state) else {
-            warning(.service, "Cannot encode watch state")
-            return
-        }
-
-        garmin.sendState(data)
-
-        guard session.isReachable else { return }
-        session.sendMessageData(data, replyHandler: nil) { error in
-            warning(.service, "Cannot send message to watch", error: error)
-        }
-    }
-
-    private func descriptionForTarget(_ target: TempTarget) -> String {
-        let units = settingsManager.settings.units
-
-        var low = target.targetBottom
-        var high = target.targetTop
-        if units == .mmolL {
-            low = low?.asMmolL
-            high = high?.asMmolL
-        }
-
-        let description =
-            "\(targetFormatter.string(from: (low ?? 0) as NSNumber)!) - \(targetFormatter.string(from: (high ?? 0) as NSNumber)!)" +
-            " for \(targetFormatter.string(from: target.duration as NSNumber)!) min"
-
-        return description
-    }
-
-    private func newBolusCalc(glucoseIds: [NSManagedObjectID], determinationId: NSManagedObjectID) async -> Decimal {
-        await context.perform {
-            let glucoseObjects = glucoseIds.compactMap { self.context.object(with: $0) as? GlucoseStored }
-            guard let determination = self.context.object(with: determinationId) as? OrefDetermination else {
-                print("Failed to fetch determination")
-                return 0
-            }
-
-            guard let firstGlucose = glucoseObjects.first else {
-                return 0 // If there's no glucose data, exit the block
-            }
-            let bg = firstGlucose.glucose // Make sure to provide a fallback value for glucose
-
-            // Calculations related to glucose data
-            var bgDelta: Int = 0
-            if glucoseObjects.count >= 3 {
-                bgDelta = Int(firstGlucose.glucose) - Int(glucoseObjects[2].glucose)
-            }
-
-            let conversion: Decimal = self.settingsManager.settings.units == .mmolL ? 0.0555 : 1
-            let isf = self.state.isf ?? 0
-            let target = determination.currentTarget as? Decimal ?? 100
-            let carbratio = determination.carbRatio as? Decimal ?? 10
-            let cob = self.state.cob ?? 0
-            let iob = self.state.iob ?? 0
-            let fattyMealFactor = self.settingsManager.settings.fattyMealFactor
-
-            // Complete bolus calculation logic
-            let targetDifference = Decimal(bg) - target
-            let targetDifferenceInsulin = targetDifference * conversion / isf
-            let fifteenMinInsulin = Decimal(bgDelta) * conversion / isf
-            let wholeCobInsulin = cob / carbratio
-            let iobInsulinReduction = -iob
-            let wholeCalc = targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin
-
-            let result = wholeCalc * self.settingsManager.settings.overrideFactor
-            var insulinCalculated: Decimal
-            if self.settingsManager.settings.fattyMeals {
-                insulinCalculated = result * fattyMealFactor
-            } else {
-                insulinCalculated = result
-            }
-
-            // Ensure the calculated insulin amount does not exceed the maximum bolus and is not below zero
-            insulinCalculated = max(min(insulinCalculated, self.settingsManager.pumpSettings.maxBolus), 0)
-            return insulinCalculated // Return the calculated insulin outside of the performAndWait block
-        }
-    }
-}
-
-extension BaseWatchManager: WCSessionDelegate {
-    func sessionDidBecomeInactive(_: WCSession) {}
-
-    func sessionDidDeactivate(_: WCSession) {}
-
-    func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
-        debug(.service, "WCSession is activated: \(state == .activated)")
-    }
-
-    func session(_: WCSession, didReceiveMessage message: [String: Any]) {
-        debug(.service, "WCSession got message: \(message)")
-
-        if let stateRequest = message["stateRequest"] as? Bool, stateRequest {
-            processQueue.async {
-                self.sendState()
-            }
-        }
-    }
-
-    func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
-        debug(.service, "WCSession got message with reply handler: \(message)")
-
-        if let carbs = message["carbs"] as? Double,
-           let fat = message["fat"] as? Double,
-           let protein = message["protein"] as? Double,
-           carbs > 0 || fat > 0 || protein > 0
-        {
-            Task {
-                await carbsStorage.storeCarbs(
-                    [CarbsEntry(
-                        id: UUID().uuidString,
-                        createdAt: Date(),
-                        actualDate: nil,
-                        carbs: Decimal(carbs),
-                        fat: Decimal(fat),
-                        protein: Decimal(protein),
-                        note: message["note"] as? String,
-                        enteredBy: CarbsEntry.local,
-                        isFPU: false,
-                        fpuID: nil
-                    )],
-                    areFetchedFromRemote: false
-                )
-
-                _ = await apsManager.determineBasal()
-                replyHandler(["confirmation": true])
-            }
-            return
-        }
-
-        if let tempTargetID = message["tempTarget"] as? String {
-            Task {
-                if var preset = tempTargetsStorage.presets().first(where: { $0.id == tempTargetID }) {
-                    preset.createdAt = Date()
-                    await tempTargetsStorage.storeTempTarget(tempTarget: preset)
-                    replyHandler(["confirmation": true])
-                } else if tempTargetID == "cancel" {
-                    let entry = TempTarget(
-                        name: TempTarget.cancel,
-                        createdAt: Date(),
-                        targetTop: 0,
-                        targetBottom: 0,
-                        duration: 0,
-                        enteredBy: TempTarget.local,
-                        reason: TempTarget.cancel,
-                        isPreset: false,
-                        enabled: false,
-                        halfBasalTarget: 160
-                    )
-                    await tempTargetsStorage.storeTempTarget(tempTarget: entry)
-                    replyHandler(["confirmation": true])
-                } else {
-                    replyHandler(["confirmation": false])
-                }
-            }
-            return
-        }
-
-        if let bolus = message["bolus"] as? Double, bolus > 0 {
-            Task {
-                await apsManager.enactBolus(amount: bolus, isSMB: false)
-                replyHandler(["confirmation": true])
-            }
-            return
-        }
-
-        replyHandler(["confirmation": false])
-    }
-
-    func session(_: WCSession, didReceiveMessageData _: Data) {}
-
-    func sessionReachabilityDidChange(_ session: WCSession) {
-        if session.isReachable {
-            processQueue.async {
-                self.sendState()
-            }
-        }
-    }
-}
-
-extension BaseWatchManager:
-    SettingsObserver,
-    PumpHistoryObserver,
-    PumpSettingsObserver,
-    BasalProfileObserver,
-    TempTargetsObserver,
-    CarbsObserver,
-    PumpBatteryObserver,
-    PumpReservoirObserver
-{
-    func settingsDidChange(_: FreeAPSSettings) {
-        Task {
-            await configureState()
-        }
-    }
-
-    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
-        // TODO:
-    }
-
-    func pumpSettingsDidChange(_: PumpSettings) {
-        Task {
-            await configureState()
-        }
-    }
-
-    func basalProfileDidChange(_: [BasalProfileEntry]) {
-        // TODO:
-    }
-
-    func tempTargetsDidUpdate(_: [TempTarget]) {
-        Task {
-            await configureState()
-        }
-    }
-
-    func carbsDidUpdate(_: [CarbsEntry]) {
-        // TODO:
-    }
-
-    func pumpBatteryDidChange(_: Battery) {
-        // TODO:
-    }
-
-    func pumpReservoirDidChange(_: Decimal) {
-        // TODO:
-    }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/36x36 Circular.png


+ 0 - 26
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json

@@ -1,26 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "36x36 Circular.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : "<=145"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

+ 0 - 53
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json

@@ -1,53 +0,0 @@
-{
-  "assets" : [
-    {
-      "filename" : "Circular.imageset",
-      "idiom" : "watch",
-      "role" : "circular"
-    },
-    {
-      "filename" : "Extra Large.imageset",
-      "idiom" : "watch",
-      "role" : "extra-large"
-    },
-    {
-      "filename" : "Graphic Bezel.imageset",
-      "idiom" : "watch",
-      "role" : "graphic-bezel"
-    },
-    {
-      "filename" : "Graphic Circular.imageset",
-      "idiom" : "watch",
-      "role" : "graphic-circular"
-    },
-    {
-      "filename" : "Graphic Corner.imageset",
-      "idiom" : "watch",
-      "role" : "graphic-corner"
-    },
-    {
-      "filename" : "Graphic Extra Large.imageset",
-      "idiom" : "watch",
-      "role" : "graphic-extra-large"
-    },
-    {
-      "filename" : "Graphic Large Rectangular.imageset",
-      "idiom" : "watch",
-      "role" : "graphic-large-rectangular"
-    },
-    {
-      "filename" : "Modular.imageset",
-      "idiom" : "watch",
-      "role" : "modular"
-    },
-    {
-      "filename" : "Utilitarian.imageset",
-      "idiom" : "watch",
-      "role" : "utilitarian"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/203x203 Square.png


+ 0 - 26
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json

@@ -1,26 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "203x203 Square.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : "<=145"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/84x84 Square.png


+ 0 - 21
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json

@@ -1,21 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "84x84 Square.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/84x84 Circular.png


+ 0 - 21
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json

@@ -1,21 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "84x84 Circular.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/40x40 Square.png


+ 0 - 21
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json

@@ -1,21 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "40x40 Square.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/240x240 Square.png


+ 0 - 26
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json

@@ -1,26 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "240x240 Square.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : "<=145"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/300x94.png


+ 0 - 21
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json

@@ -1,21 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "300x94.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/58x58 Square.png


+ 0 - 26
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json

@@ -1,26 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "58x58 Square.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : "<=145"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

BIN
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/44x44 Square.png


+ 0 - 26
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json

@@ -1,26 +0,0 @@
-{
-  "images" : [
-    {
-      "filename" : "44x44 Square.png",
-      "idiom" : "watch",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : "<=145"
-    },
-    {
-      "idiom" : "watch",
-      "scale" : "2x",
-      "screen-width" : ">183"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  },
-  "properties" : {
-    "auto-scaling" : "auto"
-  }
-}

+ 0 - 106
FreeAPSWatch WatchKit Extension/ComplicationController.swift

@@ -1,106 +0,0 @@
-import ClockKit
-import SwiftUI
-
-class ComplicationController: NSObject, CLKComplicationDataSource {
-    // MARK: - Complication Configuration
-
-    func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
-        let descriptors = [
-            CLKComplicationDescriptor(
-                identifier: "complication",
-                displayName: "Trio",
-                supportedFamilies: [
-                    .graphicCorner,
-                    .graphicCircular,
-                    .modularSmall,
-                    .utilitarianSmall,
-                    .circularSmall
-                ]
-            )
-        ]
-
-        // Call the handler with the currently supported complication descriptors
-        handler(descriptors)
-    }
-
-    func handleSharedComplicationDescriptors(_: [CLKComplicationDescriptor]) {
-        // Do any necessary work to support these newly shared complication descriptors
-    }
-
-    // MARK: - Timeline Configuration
-
-    func getTimelineEndDate(for _: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
-        // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines
-        handler(nil)
-    }
-
-    func getPrivacyBehavior(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
-        // Call the handler with your desired behavior when the device is locked
-        handler(.showOnLockScreen)
-    }
-
-    // MARK: - Timeline Population
-
-    func getCurrentTimelineEntry(
-        for complication: CLKComplication,
-        withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void
-    ) {
-        switch complication.family {
-        case .graphicCorner:
-            guard let image = UIImage(named: "Complication/Graphic Corner") else {
-                handler(nil)
-                return
-            }
-            let template = CLKComplicationTemplateGraphicCornerTextImage(
-                textProvider: CLKTextProvider(format: "%@", "Trio"),
-                imageProvider: CLKFullColorImageProvider(fullColorImage: image)
-            )
-            let timelineEntry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
-            handler(timelineEntry)
-        case .modularSmall:
-            let template = CLKComplicationTemplateModularSmallRingText(
-                textProvider: CLKTextProvider(format: "%@", "Trio"),
-                fillFraction: 1,
-                ringStyle: .closed
-            )
-
-            let timelineEntry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
-            handler(timelineEntry)
-        case .utilitarianSmall:
-            guard let image = UIImage(named: "Complication/Utilitarian") else {
-                handler(nil)
-                return
-            }
-            let template = CLKComplicationTemplateUtilitarianSmallSquare(
-                imageProvider: CLKImageProvider(onePieceImage: image)
-            )
-            let timelineEntry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
-            handler(timelineEntry)
-        case .circularSmall:
-            let template =
-                CLKComplicationTemplateCircularSmallSimpleText(textProvider: CLKTextProvider(format: "%@", "Trio"))
-            let timelineEntry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template)
-            handler(timelineEntry)
-        default:
-            handler(nil)
-        }
-    }
-
-    func getTimelineEntries(
-        for _: CLKComplication,
-        after _: Date,
-        limit _: Int,
-        withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void
-    ) {
-        handler(nil)
-    }
-
-    // MARK: - Sample Templates
-
-    func getLocalizableSampleTemplate(
-        for _: CLKComplication,
-        withHandler handler: @escaping (CLKComplicationTemplate?) -> Void
-    ) {
-        handler(nil)
-    }
-}

+ 0 - 33
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -1,33 +0,0 @@
-import Foundation
-
-struct WatchState: Codable {
-    var glucose: String?
-    var trend: String?
-    var trendRaw: String?
-    var delta: String?
-    var glucoseDate: Date?
-    var lastLoopDate: Date?
-    var lastLoopDateInterval: UInt64?
-    var bolusIncrement: Decimal?
-    var maxCOB: Decimal?
-    var maxBolus: Decimal?
-    var carbsRequired: Decimal?
-    var bolusRecommended: Decimal?
-    var iob: Decimal?
-    var cob: Decimal?
-    var tempTargets: [TempTargetWatchPreset] = []
-    var eventualBG: String?
-    var eventualBGRaw: String?
-    var displayOnWatch: AwConfig?
-    var displayFatAndProteinOnWatch: Bool?
-    var confirmBolusFaster: Bool?
-    var isf: Decimal?
-    var override: String?
-}
-
-struct TempTargetWatchPreset: Codable, Identifiable {
-    let name: String
-    let id: String
-    let description: String
-    let until: Date?
-}

+ 0 - 15
FreeAPSWatch WatchKit Extension/FreeAPSApp.swift

@@ -1,15 +0,0 @@
-import SwiftUI
-
-@main struct FreeAPSApp: App {
-    @StateObject var state = WatchStateModel()
-
-    @SceneBuilder var body: some Scene {
-        WindowGroup {
-            NavigationView {
-                MainView()
-            }.environmentObject(state)
-        }
-
-//        WKNotificationScene(controller: NotificationController.self, category: "FreeAPSCategory")
-    }
-}

+ 0 - 16
FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>com.apple.developer.healthkit</key>
-	<true/>
-	<key>com.apple.developer.healthkit.access</key>
-	<array/>
-	<key>com.apple.developer.healthkit.background-delivery</key>
-	<true/>
-	<key>com.apple.security.application-groups</key>
-	<array>
-		<string>$(APP_GROUP_ID)</string>
-	</array>
-</dict>
-</plist>

+ 0 - 22
FreeAPSWatch WatchKit Extension/Info.plist

@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-	<key>NSHealthClinicalHealthRecordsShareUsageDescription</key>
-	<string>Bla bla Record Health</string>
-	<key>NSHealthShareUsageDescription</key>
-	<string>Bla bla Share Health</string>
-	<key>NSHealthUpdateUsageDescription</key>
-	<string>Bla bla Update Health</string>
-	<key>NSExtension</key>
-	<dict>
-		<key>NSExtensionAttributes</key>
-		<dict>
-			<key>WKAppBundleIdentifier</key>
-			<string>$(BUNDLE_IDENTIFIER).watchkitapp</string>
-		</dict>
-		<key>NSExtensionPointIdentifier</key>
-		<string>com.apple.watchkit</string>
-	</dict>
-</dict>
-</plist>

+ 0 - 25
FreeAPSWatch WatchKit Extension/NotificationController.swift

@@ -1,25 +0,0 @@
-import SwiftUI
-import UserNotifications
-import WatchKit
-
-class NotificationController: WKUserNotificationHostingController<NotificationView> {
-    override var body: NotificationView {
-        NotificationView()
-    }
-
-    override func willActivate() {
-        // This method is called when watch view controller is about to be visible to user
-        super.willActivate()
-    }
-
-    override func didDeactivate() {
-        // This method is called when watch view controller is no longer visible
-        super.didDeactivate()
-    }
-
-    override func didReceive(_: UNNotification) {
-        // This method is called when a notification needs to be presented.
-        // Implement it if you use a dynamic notification interface.
-        // Populate your dynamic notification interface as quickly as possible.
-    }
-}

+ 0 - 13
FreeAPSWatch WatchKit Extension/NotificationView.swift

@@ -1,13 +0,0 @@
-import SwiftUI
-
-struct NotificationView: View {
-    var body: some View {
-        Text("Hello, World!")
-    }
-}
-
-struct NotificationView_Previews: PreviewProvider {
-    static var previews: some View {
-        NotificationView()
-    }
-}

+ 0 - 20
FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns

@@ -1,20 +0,0 @@
-{
-    "aps": {
-        "alert": {
-            "body": "Test message",
-            "title": "Optional title",
-            "subtitle": "Optional subtitle"
-        },
-        "category": "myCategory",
-        "thread-id": "5280"
-    },
-    
-    "WatchKit Simulator Actions": [
-        {
-            "title": "First Button",
-            "identifier": "firstButtonAction"
-        }
-    ],
-    
-    "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
-}

+ 0 - 118
FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift

@@ -1,118 +0,0 @@
-import Combine
-import SwiftUI
-
-struct BolusConfirmationView: View {
-    @EnvironmentObject var state: WatchStateModel
-
-    @State var isCrownLeftOriented = WKInterfaceDevice.current().crownOrientation == .left
-    @State var crownProgress: CGFloat = 100.0
-    @State var progress: CGFloat = 0
-
-    private let elementSize: CGFloat = 30
-
-    @State var progressReturn: AnyCancellable?
-
-    @State var done = false
-
-    var body: some View {
-        VStack {
-            GeometryReader { geo in
-                HStack(alignment: .top) {
-                    Spacer().frame(width: elementSize / 2)
-                    ZStack(alignment: .top) {
-                        RoundedRectangle(cornerRadius: elementSize / 2, style: .circular)
-                            .fill(.secondary)
-                            .frame(width: elementSize, height: geo.size.height)
-                            .opacity(0.2)
-
-                        RoundedRectangle(cornerRadius: elementSize / 2, style: .circular)
-                            .fill(Color.insulin)
-                            .frame(width: elementSize, height: elementSize + (geo.size.height - elementSize) * progress / 100)
-                            .opacity(0.2)
-
-                        Image(systemName: done == true ? "checkmark.circle.fill" : "arrow.down.circle.fill")
-                            .resizable()
-                            .foregroundColor(done == true ? .loopGreen : .insulin)
-                            .frame(width: elementSize, height: elementSize)
-                            .offset(y: (geo.size.height - elementSize) * progress / 100)
-
-                    }.frame(maxWidth: .infinity, alignment: .center)
-                    if isCrownLeftOriented {
-                        Spacer().frame(width: elementSize / 2)
-                    } else {
-                        Image(systemName: "digitalcrown.arrow.counterclockwise.fill")
-                            .resizable()
-                            .frame(width: elementSize / 2, height: elementSize / 2)
-                            .foregroundColor(.primary)
-                            .transition(.opacity)
-                    }
-                }.frame(maxWidth: .infinity, maxHeight: .infinity)
-            }
-            .padding()
-            HStack(spacing: 16) {
-                if isCrownLeftOriented {
-                    Image(systemName: "digitalcrown.arrow.counterclockwise.fill")
-                        .resizable()
-                        .frame(width: elementSize / 2, height: elementSize / 2)
-                        .foregroundColor(.primary)
-                        .transition(.opacity)
-                }
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    state.pendingBolus = nil
-                    state.isConfirmationBolusViewActive = false
-                }
-                label: {
-                    Text("Cancel")
-                }
-                if isCrownLeftOriented {
-                    Spacer().frame(width: elementSize / 2)
-                }
-            }
-        }
-        .focusable(true)
-        .digitalCrownRotation(
-            $crownProgress,
-            from: 0.0,
-            through: 100.0,
-            by: state.confirmBolusFaster ? 5 : 0.5,
-            sensitivity: .high,
-            isContinuous: false,
-            isHapticFeedbackEnabled: true
-        )
-        .onChange(of: crownProgress) { _ in
-            guard !done else { return }
-
-            progressReturn?.cancel()
-            progress = min(max(0, 100 - crownProgress), 100)
-            if progress >= 100 {
-                success()
-            } else {
-                progressReturn = Just(())
-                    .delay(for: 0.1, scheduler: RunLoop.main)
-                    .sink { _ in
-                        crownProgress = 100
-                        withAnimation {
-                            progress = 0
-                        }
-                    }
-            }
-        }
-    }
-
-    private func success() {
-        WKInterfaceDevice.current().play(.success)
-        withAnimation {
-            done = true
-        }
-        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
-            state.enactBolus()
-        }
-    }
-}
-
-struct BolusConfirmationView_Previews: PreviewProvider {
-    static var previews: some View {
-        BolusConfirmationView(progress: 50, done: false).environmentObject(WatchStateModel())
-    }
-}

+ 0 - 100
FreeAPSWatch WatchKit Extension/Views/BolusView.swift

@@ -1,100 +0,0 @@
-import SwiftUI
-
-struct BolusView: View {
-    @EnvironmentObject var state: WatchStateModel
-
-    @State var steps = 0.0
-
-    var numberFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.minimum = 0
-        formatter.maximum = Double((state.maxBolus ?? 5) / (state.bolusIncrement ?? 0.1)) as NSNumber
-        formatter.maximumFractionDigits = (state.bolusIncrement ?? 0.1) > 0.05 ? 1 : 2
-        formatter.minimumFractionDigits = (state.bolusIncrement ?? 0.1) > 0.05 ? 1 : 2
-        formatter.allowsFloats = true
-        formatter.roundingIncrement = Double(state.bolusIncrement ?? 0.1) as NSNumber
-        return formatter
-    }
-
-    var body: some View {
-        GeometryReader { geo in
-            VStack(spacing: 16) {
-                HStack {
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        let newValue = steps - 1
-                        steps = max(newValue, 0)
-                    } label: { Image(systemName: "minus") }
-                        .frame(width: geo.size.width / 4)
-                    Spacer()
-                    Text(numberFormatter.string(from: (steps * Double(state.bolusIncrement ?? 0.1)) as NSNumber)! + " U")
-                        .font(.headline)
-                        .focusable(true)
-                        .digitalCrownRotation(
-                            $steps,
-                            from: 0,
-                            through: Double((state.maxBolus ?? 5) / (state.bolusIncrement ?? 0.1)),
-                            by: 1,
-                            sensitivity: .medium,
-                            isContinuous: false,
-                            isHapticFeedbackEnabled: true
-                        )
-                    Spacer()
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        let newValue = steps + 1
-                        steps = min(newValue, Double((state.maxBolus ?? 5) / (state.bolusIncrement ?? 0.1)))
-                    } label: { Image(systemName: "plus") }
-                        .frame(width: geo.size.width / 4)
-                }
-
-                HStack {
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        state.isBolusViewActive = false
-                    }
-                    label: {
-                        Image(systemName: "xmark.circle.fill")
-                            .resizable()
-                            .foregroundColor(.loopRed)
-                            .frame(width: 30, height: 30)
-                    }
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        enactBolus()
-                    }
-                    label: {
-                        Image(systemName: "checkmark.circle.fill")
-                            .resizable()
-                            .foregroundColor(.loopGreen)
-                            .frame(width: 30, height: 30)
-                    }
-                    .disabled(steps <= 0)
-                }
-            }.frame(maxHeight: .infinity)
-        }
-        .navigationTitle("Enact Bolus")
-        .onAppear {
-            steps = Double((state.bolusRecommended ?? 0) / (state.bolusIncrement ?? 0.1))
-        }
-    }
-
-    private func enactBolus() {
-        let amount = steps * Double(state.bolusIncrement ?? 0.1)
-        state.addBolus(amount: amount)
-    }
-}
-
-struct BolusView_Previews: PreviewProvider {
-    static var previews: some View {
-        let state = WatchStateModel()
-        state.bolusRecommended = 10.3
-        state.bolusIncrement = 0.05
-        return Group {
-            BolusView()
-            BolusView().previewDevice("Apple Watch Series 5 - 40mm")
-            BolusView().previewDevice("Apple Watch Series 3 - 38mm")
-        }.environmentObject(state)
-    }
-}

+ 0 - 253
FreeAPSWatch WatchKit Extension/Views/CarbsView.swift

@@ -1,253 +0,0 @@
-import SwiftUI
-
-struct CarbsView: View {
-    @EnvironmentObject var state: WatchStateModel
-
-    // Selected nutrient
-    enum Selection: String {
-        case carbs
-        case protein
-        case fat
-    }
-
-    @State var selection: Selection = .carbs
-    @State var carbAmount = 0.0
-    @State var fatAmount = 0.0
-    @State var proteinAmount = 0.0
-    @State var colorOfselection: Color = .darkGray
-    // @State var displayPresets: Bool = false
-
-    var numberFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.numberStyle = .decimal
-        formatter.minimum = 0
-        formatter.maximum = (state.maxCOB ?? 120) as NSNumber
-        formatter.maximumFractionDigits = 0
-        formatter.allowsFloats = false
-        return formatter
-    }
-
-    var body: some View {
-        VStack {
-            // nutrient
-            carbs
-            if state.displayFatAndProteinOnWatch {
-                Spacer()
-                fat
-                Spacer()
-                protein
-            }
-            buttonStack
-        }
-        .onAppear { carbAmount = Double(state.carbsRequired ?? 0) }
-    }
-
-    var nutrient: some View {
-        HStack {
-            switch selection {
-            case .protein:
-                Text("Protein")
-            case .fat:
-                Text("Fat")
-            default:
-                Text("Carbs")
-            }
-        }.font(.footnote).frame(maxWidth: .infinity, alignment: .center)
-    }
-
-    var carbs: some View {
-        HStack {
-            if selection == .carbs {
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    let newValue = carbAmount - 5
-                    carbAmount = max(newValue, 0)
-                }
-                label: {
-                    HStack {
-                        Image(systemName: "minus")
-                        Text("") // Ugly fix to increase active tapping (button) area.
-                    }
-                }
-                .buttonStyle(.borderless).padding(.leading, 5)
-                .tint(selection == .carbs ? .blue : .none)
-            }
-            Spacer()
-            Text("🥨")
-            Spacer()
-            Text(numberFormatter.string(from: carbAmount as NSNumber)! + " g")
-                .font(selection == .carbs ? .title : .title3)
-                .focusable(selection == .carbs)
-                .digitalCrownRotation(
-                    $carbAmount,
-                    from: 0,
-                    through: Double(state.maxCOB ?? 120),
-                    by: 1,
-                    sensitivity: .medium,
-                    isContinuous: false,
-                    isHapticFeedbackEnabled: true
-                )
-            Spacer()
-            if selection == .carbs {
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    let newValue = carbAmount + 5
-                    carbAmount = min(newValue, Double(state.maxCOB ?? 120))
-                } label: { Image(systemName: "plus") }
-                    .buttonStyle(.borderless).padding(.trailing, 5)
-                    .tint(selection == .carbs ? .blue : .none)
-            }
-        }
-        .minimumScaleFactor(0.7)
-        .onTapGesture {
-            select(entry: .carbs)
-        }
-        .background(selection == .carbs && state.displayFatAndProteinOnWatch ? colorOfselection : .black)
-        .padding(.top)
-    }
-
-    var protein: some View {
-        HStack {
-            if selection == .protein {
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    let newValue = proteinAmount - 5
-                    proteinAmount = max(newValue, 0)
-                } label: {
-                    HStack {
-                        Image(systemName: "minus")
-                        Text("") // Ugly fix to increase active tapping (button) area.
-                    }
-                }
-                .buttonStyle(.borderless).padding(.leading, 5)
-                .tint(selection == .protein ? .blue : .none)
-            }
-            Spacer()
-            Text("🍗")
-            Spacer()
-            Text(numberFormatter.string(from: proteinAmount as NSNumber)! + " g")
-                .font(selection == .protein ? .title : .title3)
-                .foregroundStyle(.red)
-                .focusable(selection == .protein)
-                .digitalCrownRotation(
-                    $proteinAmount,
-                    from: 0,
-                    through: Double(240),
-                    by: 1,
-                    sensitivity: .medium,
-                    isContinuous: false,
-                    isHapticFeedbackEnabled: true
-                )
-            Spacer()
-            if selection == .protein {
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    let newValue = proteinAmount + 5
-                    proteinAmount = min(newValue, Double(240))
-                } label: { Image(systemName: "plus") }.buttonStyle(.borderless).padding(.trailing, 5)
-                    .tint(selection == .protein ? .blue : .none)
-            }
-        }
-        .minimumScaleFactor(0.7)
-        .onTapGesture {
-            select(entry: .protein)
-        }
-        .background(selection == .protein ? colorOfselection : .black)
-    }
-
-    var fat: some View {
-        HStack {
-            if selection == .fat {
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    let newValue = fatAmount - 5
-                    fatAmount = max(newValue, 0)
-                } label: {
-                    HStack {
-                        Image(systemName: "minus")
-                        Text("") // Ugly fix to increase active tapping (button) area.
-                    }
-                }
-                .buttonStyle(.borderless).padding(.leading, 5)
-                .tint(selection == .fat ? .blue : .none)
-            }
-            Spacer()
-            Text("🧀")
-            Spacer()
-            Text(numberFormatter.string(from: fatAmount as NSNumber)! + " g")
-                .font(selection == .fat ? .title : .title3)
-                .foregroundColor(.loopYellow)
-                .focusable(selection == .fat)
-                .digitalCrownRotation(
-                    $fatAmount,
-                    from: 0,
-                    through: Double(240),
-                    by: 1,
-                    sensitivity: .medium,
-                    isContinuous: false,
-                    isHapticFeedbackEnabled: true
-                )
-            Spacer()
-            if selection == .fat {
-                Button {
-                    WKInterfaceDevice.current().play(.click)
-                    let newValue = fatAmount + 5
-                    fatAmount = min(newValue, Double(240))
-                } label: { Image(systemName: "plus") }
-                    .buttonStyle(.borderless).padding(.trailing, 5)
-                    .tint(selection == .fat ? .blue : .none)
-            }
-        }
-        .minimumScaleFactor(0.7)
-        .onTapGesture {
-            select(entry: .fat)
-        }
-        .background(selection == .fat ? colorOfselection : .black)
-    }
-
-    var buttonStack: some View {
-        HStack(spacing: 25) {
-            /* To do: display the actual meal presets
-             Button {
-                 displayPresets.toggle()
-             }
-             label: { Image(systemName: "menucard.fill") }
-                 .buttonStyle(.borderless)
-             */
-            Button {
-                WKInterfaceDevice.current().play(.click)
-                // Get amount from displayed string
-                let amountCarbs = Int(numberFormatter.string(from: carbAmount as NSNumber)!) ?? Int(carbAmount.rounded())
-                let amountFat = Int(numberFormatter.string(from: fatAmount as NSNumber)!) ?? Int(fatAmount.rounded())
-                let amountProtein = Int(numberFormatter.string(from: proteinAmount as NSNumber)!) ??
-                    Int(proteinAmount.rounded())
-                let note = "Via Watch" // Hard-coded note for entries from watch
-                state.addMeal(amountCarbs, fat: amountFat, protein: amountProtein, note: note)
-            }
-            label: { Text("Save") }
-                .buttonStyle(.borderless)
-                .font(.callout)
-                .foregroundColor(carbAmount > 0 || fatAmount > 0 || proteinAmount > 0 ? .blue : .secondary)
-                .disabled(carbAmount <= 0 && fatAmount <= 0 && proteinAmount <= 0)
-        }
-        .frame(maxHeight: .infinity, alignment: .bottom)
-        .padding(.top)
-    }
-
-    private func select(entry: Selection) {
-        selection = entry
-    }
-}
-
-struct CarbsView_Previews: PreviewProvider {
-    static var previews: some View {
-        let state = WatchStateModel()
-        state.carbsRequired = 120
-        return Group {
-            CarbsView()
-            CarbsView().previewDevice("Apple Watch Series 5 - 40mm")
-            CarbsView().previewDevice("Apple Watch Series 3 - 38mm")
-        }
-        .environmentObject(state)
-    }
-}

+ 0 - 86
FreeAPSWatch WatchKit Extension/Views/ConfirmationView.swift

@@ -1,86 +0,0 @@
-import SwiftUI
-
-struct ConfirmationView: View {
-    @Binding var success: Bool?
-
-    var body: some View {
-        ZStack {
-            Group {
-                Image(systemName: "checkmark.circle.fill")
-                    .resizable()
-                    .foregroundColor(.loopGreen)
-                    .opacity(success == true ? 1.0 : 0.0)
-                    .scaleEffect(success == true ? 1.0 : 0.0)
-
-                Image(systemName: "xmark.circle.fill")
-                    .resizable()
-                    .foregroundColor(.loopRed)
-                    .opacity(success == false ? 1.0 : 0.0)
-                    .scaleEffect(success == false ? 1.0 : 0.0)
-
-                BlinkingView(count: 10, size: 10)
-                    .opacity(success == nil ? 1.0 : 0.0)
-                    .scaleEffect(success == nil ? 1.0 : 0.0)
-            }
-            .frame(width: 50, height: 50)
-        }
-        .frame(maxWidth: .infinity, maxHeight: .infinity)
-    }
-}
-
-struct ConfirmationView_Previews: PreviewProvider {
-    struct Container: View {
-        @State var success: Bool?
-
-        var body: some View {
-            ConfirmationView(success: $success)
-        }
-    }
-
-    static var previews: some View {
-        Container()
-    }
-}
-
-struct BlinkingView: View {
-    let count: UInt
-    let size: CGFloat
-
-    var body: some View {
-        GeometryReader { geometry in
-            ForEach(0 ..< Int(count)) { index in
-                item(forIndex: index, in: geometry.size)
-                    .frame(width: geometry.size.width, height: geometry.size.height)
-            }
-        }
-        .animation(.none, value: false)
-        .aspectRatio(contentMode: .fit)
-        .onAppear {
-            scale = 1
-            opacity = 1
-        }
-    }
-
-    @State var scale = 0.5
-    @State var opacity = 0.25
-
-    func animation(index: Int) -> Animation {
-        Animation
-            .default
-            .repeatCount(.max, autoreverses: true)
-            .delay(Double(index) / Double(count) / 2)
-    }
-
-    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
-        let angle = 2 * CGFloat.pi / CGFloat(count) * CGFloat(index)
-        let x = (geometrySize.width / 2 - size / 2) * cos(angle)
-        let y = (geometrySize.height / 2 - size / 2) * sin(angle)
-        return Circle()
-            .frame(width: size, height: size)
-            .scaleEffect(scale)
-            .opacity(opacity)
-            .animation(animation(index: index), value: scale)
-            .animation(animation(index: index), value: opacity)
-            .offset(x: x, y: y)
-    }
-}

+ 0 - 444
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -1,444 +0,0 @@
-import HealthKit
-import SwiftDate
-import SwiftUI
-
-struct MainView: View {
-    private enum Config {
-        static let lag: TimeInterval = 30
-    }
-
-    @EnvironmentObject var state: WatchStateModel
-
-    @State var isCarbsActive = false
-    @State var isTargetsActive = false
-    @State var isBolusActive = false
-    @State private var pulse = 0
-    @State private var steps = 0
-
-    @GestureState var isDetectingLongPress = false
-    @State var completedLongPress = false
-
-    @State var completedLongPressOfBG = false
-    @GestureState var isDetectingLongPressOfBG = false
-
-    private var healthStore = HKHealthStore()
-    let heartRateQuantity = HKUnit(from: "count/min")
-
-    var body: some View {
-        ZStack(alignment: .topLeading) {
-            if !completedLongPressOfBG {
-                if state.timerDate.timeIntervalSince(state.lastUpdate) > 10 {
-                    HStack {
-                        withAnimation {
-                            BlinkingView(count: 5, size: 3)
-                                .frame(width: 14, height: 14)
-                                .padding(2)
-                        }
-                        Text("Updating...").font(.caption2).foregroundColor(.secondary)
-                    }
-                }
-            }
-            VStack {
-                if !completedLongPressOfBG {
-                    header
-                    Spacer()
-                    buttons
-                } else {
-                    bigHeader
-                }
-            }
-
-            if state.isConfirmationViewActive {
-                ConfirmationView(success: $state.confirmationSuccess)
-                    .background(Rectangle().fill(.black))
-            }
-
-            if state.isConfirmationBolusViewActive {
-                BolusConfirmationView()
-                    .environmentObject(state)
-                    .background(Rectangle().fill(.black))
-            }
-        }
-        .frame(maxHeight: .infinity)
-        .padding()
-        .onReceive(state.timer) { date in
-            state.timerDate = date
-            state.requestState()
-        }
-        .onAppear {
-            state.requestState()
-        }
-    }
-
-    var header: some View {
-        VStack {
-            HStack(alignment: .top) {
-                VStack(alignment: .leading) {
-                    HStack {
-                        Text(state.glucose).font(.title)
-                        Text(state.trend)
-                            .scaledToFill()
-                            .minimumScaleFactor(0.5)
-                    }
-                    /* IF YOU WANT TO DISPLAY MINUTES AGO, UNCOMMENT the gray code below
-                     let minutesAgo: TimeInterval = -1 * (state.glucoseDate ?? .distantPast).timeIntervalSinceNow / 60
-                     let minuteString = minutesAgo.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0)))
-                     */
-                    HStack {
-                        /* if minutesAgo > 0 {
-                             Text(minuteString)
-                             Text("min")
-                         } */
-                        Text(state.delta)
-                    }
-                    .font(.caption2).foregroundColor(.gray)
-                }
-                Spacer()
-
-                VStack(spacing: 0) {
-                    HStack {
-                        Circle().stroke(color, lineWidth: 5).frame(width: 26, height: 26).padding(10)
-                    }
-
-                    if state.lastLoopDate != nil {
-                        Text(timeString).font(.caption2).foregroundColor(.gray)
-                    } else {
-                        Text("--").font(.caption2).foregroundColor(.gray)
-                    }
-                }
-            }
-            Spacer()
-            HStack(alignment: .firstTextBaseline) {
-                Text(iobFormatter.string(from: (state.cob ?? 0) as NSNumber)!)
-                    .font(.caption2)
-                    .scaledToFill()
-                    .foregroundColor(Color.white)
-                    .minimumScaleFactor(0.5)
-                Text("g").foregroundColor(.loopYellow)
-                    .font(.caption2)
-                    .scaledToFill()
-                    .minimumScaleFactor(0.5)
-                Spacer()
-                Text(iobFormatter.string(from: (state.iob ?? 0) as NSNumber)!)
-                    .font(.caption2)
-                    .scaledToFill()
-                    .foregroundColor(Color.white)
-                    .minimumScaleFactor(0.5)
-
-                Text("U").foregroundColor(.insulin)
-                    .font(.caption2)
-                    .scaledToFill()
-                    .minimumScaleFactor(0.5)
-
-                switch state.displayOnWatch {
-                case .HR:
-                    Spacer()
-                    HStack {
-                        if completedLongPress {
-                            HStack {
-                                Text("❤️" + " \(pulse)")
-                                    .fontWeight(.regular)
-                                    .font(.custom("activated", size: 20))
-                                    .scaledToFill()
-                                    .foregroundColor(.white)
-                                    .minimumScaleFactor(0.5)
-                            }
-                            .scaleEffect(isDetectingLongPress ? 3 : 1)
-                            .gesture(longPress)
-
-                        } else {
-                            HStack {
-                                Text("❤️" + " \(pulse)")
-                                    .fontWeight(.regular)
-                                    .font(.caption2)
-                                    .scaledToFill()
-                                    .foregroundColor(.white)
-                                    .minimumScaleFactor(0.5)
-                            }
-                            .scaleEffect(isDetectingLongPress ? 3 : 1)
-                            .gesture(longPress)
-                        }
-                    }
-                case .BGTarget:
-                    if let eventualBG = state.eventualBG.nonEmpty {
-                        Spacer()
-                        HStack {
-                            Text(eventualBG)
-                                .font(.caption2)
-                                .scaledToFill()
-                                .foregroundColor(.secondary)
-                                .minimumScaleFactor(0.5)
-                        }
-                    }
-                case .steps:
-                    Spacer()
-                    HStack {
-                        Text("🦶" + " \(steps)")
-                            .fontWeight(.regular)
-                            .font(.caption2)
-                            .scaledToFill()
-                            .foregroundColor(.white)
-                            .minimumScaleFactor(0.5)
-                    }
-                case .isf:
-                    Spacer()
-                    let isf: String = state.isf != nil ? "\(state.isf ?? 0)" : "-"
-                    HStack {
-                        Image(systemName: "arrow.up.arrow.down")
-                            .renderingMode(.template)
-                            .resizable()
-                            .frame(width: 12, height: 12)
-                            .foregroundColor(.loopGreen)
-                        Text("\(isf)")
-                            .fontWeight(.regular)
-                            .font(.caption2)
-                            .scaledToFill()
-                            .foregroundColor(.white)
-                            .minimumScaleFactor(0.5)
-                    }
-                case .override:
-                    Spacer()
-                    let override: String = state.override != nil ? state.override! : "-"
-                    HStack {
-                        Text("👤 \(override)")
-                            .fontWeight(.regular)
-                            .font(.caption2)
-                            .scaledToFill()
-                            .foregroundColor(.white)
-                            .minimumScaleFactor(0.5)
-                    }
-                }
-            }
-            Spacer()
-                .onAppear(perform: start)
-        }
-        .padding()
-        // .scaleEffect(isDetectingLongPressOfBG ? 3 : 1)
-        .gesture(longPresBGs)
-    }
-
-    var bigHeader: some View {
-        VStack(alignment: .center) {
-            HStack {
-                Text(state.glucose).font(.custom("Big BG", size: 55))
-                Text(state.trend != "→" ? state.trend : "")
-                    .scaledToFill()
-                    .minimumScaleFactor(0.5)
-            }.padding(.bottom, 35)
-
-            HStack {
-                Circle().stroke(color, lineWidth: 5).frame(width: 20, height: 20).padding(10)
-            }
-        }
-        .gesture(longPresBGs)
-    }
-
-    var longPress: some Gesture {
-        LongPressGesture(minimumDuration: 1)
-            .updating($isDetectingLongPress) { currentState, gestureState,
-                _ in
-                gestureState = currentState
-            }
-            .onEnded { _ in
-                if completedLongPress {
-                    completedLongPress = false
-                } else { completedLongPress = true }
-            }
-    }
-
-    var longPresBGs: some Gesture {
-        LongPressGesture(minimumDuration: 1)
-            .updating($isDetectingLongPressOfBG) { currentState, gestureState,
-                _ in
-                gestureState = currentState
-            }
-            .onEnded { _ in
-                if completedLongPressOfBG {
-                    completedLongPressOfBG = false
-                } else { completedLongPressOfBG = true }
-            }
-    }
-
-    var buttons: some View {
-        HStack(alignment: .center) {
-            NavigationLink(isActive: $state.isCarbsViewActive) {
-                CarbsView()
-                    .environmentObject(state)
-            } label: {
-                Image("carbs", bundle: nil)
-                    .renderingMode(.template)
-                    .resizable()
-                    .frame(width: 24, height: 24)
-                    .foregroundColor(.loopYellow)
-            }
-
-            NavigationLink(isActive: $state.isTempTargetViewActive) {
-                TempTargetsView()
-                    .environmentObject(state)
-            } label: {
-                VStack {
-                    Image("target", bundle: nil)
-                        .renderingMode(.template)
-                        .resizable()
-                        .frame(width: 24, height: 24)
-                        .foregroundColor(.loopGreen)
-                    if let until = state.tempTargets.compactMap(\.until).first, until > Date() {
-                        Text(until, style: .timer)
-                            .scaledToFill()
-                            .font(.system(size: 8))
-                    }
-                }
-            }
-
-            NavigationLink(isActive: $state.isBolusViewActive) {
-                BolusView()
-                    .environmentObject(state)
-            } label: {
-                Image("bolus", bundle: nil)
-                    .renderingMode(.template)
-                    .resizable()
-                    .frame(width: 24, height: 24)
-                    .foregroundColor(.insulin)
-            }
-        }
-    }
-
-    func start() {
-        autorizeHealthKit()
-        startHeartRateQuery(quantityTypeIdentifier: .heartRate)
-        startStepsQuery(quantityTypeIdentifier: .stepCount)
-    }
-
-    func autorizeHealthKit() {
-        let healthKitTypes: Set = [
-            HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)!,
-            HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!
-        ]
-        healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { _, _ in }
-    }
-
-    private func startStepsQuery(quantityTypeIdentifier _: HKQuantityTypeIdentifier) {
-        let type = HKQuantityType.quantityType(forIdentifier: .stepCount)!
-        let now = Date()
-        let startOfDay = Calendar.current.startOfDay(for: now)
-        var interval = DateComponents()
-        interval.day = 1
-        let query = HKStatisticsCollectionQuery(
-            quantityType: type,
-            quantitySamplePredicate: nil,
-            options: [.cumulativeSum],
-            anchorDate: startOfDay,
-            intervalComponents: interval
-        )
-
-        query.initialResultsHandler = { _, result, _ in
-            var resultCount = 0.0
-            guard let result = result else {
-                self.steps = 0
-                return
-            }
-            result.enumerateStatistics(from: startOfDay, to: now) { statistics, _ in
-
-                if let sum = statistics.sumQuantity() {
-                    // Get steps (they are of double type)
-                    resultCount = sum.doubleValue(for: HKUnit.count())
-                } // end if
-                // Return
-                self.steps = Int(resultCount)
-            }
-        }
-
-        query.statisticsUpdateHandler = {
-            _, statistics, _, _ in
-
-            // If new statistics are available
-            if let sum = statistics?.sumQuantity() {
-                let resultCount = sum.doubleValue(for: HKUnit.count())
-                // Return
-                self.steps = Int(resultCount)
-            } // end if
-        }
-        healthStore.execute(query)
-    }
-
-    private func startHeartRateQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) {
-        let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
-        let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {
-            _, samples, _, _, _ in
-            guard let samples = samples as? [HKQuantitySample] else {
-                return
-            }
-            self.process(samples, type: quantityTypeIdentifier)
-        }
-        let query = HKAnchoredObjectQuery(
-            type: HKObjectType.quantityType(forIdentifier: quantityTypeIdentifier)!,
-            predicate: devicePredicate,
-            anchor: nil,
-            limit: HKObjectQueryNoLimit,
-            resultsHandler: updateHandler
-        )
-        query.updateHandler = updateHandler
-        healthStore.execute(query)
-    }
-
-    private func process(_ samples: [HKQuantitySample], type: HKQuantityTypeIdentifier) {
-        var lastHeartRate = 0.0
-        for sample in samples {
-            if type == .heartRate {
-                lastHeartRate = sample.quantity.doubleValue(for: heartRateQuantity)
-            }
-            pulse = Int(lastHeartRate)
-        }
-    }
-
-    private var iobFormatter: NumberFormatter {
-        let formatter = NumberFormatter()
-        formatter.maximumFractionDigits = 2
-        formatter.numberStyle = .decimal
-        return formatter
-    }
-
-    private var timeString: String {
-        let minAgo = Int((Date().timeIntervalSince(state.lastLoopDate ?? .distantPast) - Config.lag) / 60) + 1
-        if minAgo > 1440 {
-            return "--"
-        }
-        return "\(minAgo) " + NSLocalizedString("min", comment: "Minutes ago since last loop")
-    }
-
-    private var color: Color {
-        guard let lastLoopDate = state.lastLoopDate else {
-            return .loopGray
-        }
-        let delta = Date().timeIntervalSince(lastLoopDate) - Config.lag
-
-        if delta <= 5.minutes.timeInterval {
-            return .loopGreen
-        } else if delta <= 10.minutes.timeInterval {
-            return .loopYellow
-        } else {
-            return .loopRed
-        }
-    }
-}
-
-struct ContentView_Previews: PreviewProvider {
-    static var previews: some View {
-        let state = WatchStateModel()
-
-        state.glucose = "15,8"
-        state.delta = "+888"
-        state.iob = 100.38
-        state.cob = 112.123
-        state.lastLoopDate = Date().addingTimeInterval(-200)
-        state
-            .tempTargets =
-            [TempTargetWatchPreset(name: "Test", id: "test", description: "", until: Date().addingTimeInterval(3600 * 3))]
-
-        return Group {
-            MainView()
-            MainView().previewDevice("Apple Watch Series 5 - 40mm")
-            MainView().previewDevice("Apple Watch Series 3 - 38mm")
-        }.environmentObject(state)
-    }
-}

+ 0 - 56
FreeAPSWatch WatchKit Extension/Views/TempTargetsView.swift

@@ -1,56 +0,0 @@
-import SwiftUI
-
-struct TempTargetsView: View {
-    @EnvironmentObject var state: WatchStateModel
-
-    var body: some View {
-        List {
-            if state.tempTargets.isEmpty {
-                Text("Set temp targets presets on iPhone first").padding()
-            } else {
-                ForEach(state.tempTargets) { target in
-                    Button {
-                        WKInterfaceDevice.current().play(.click)
-                        state.enactTempTarget(id: target.id)
-                    } label: {
-                        VStack(alignment: .leading) {
-                            HStack {
-                                Text(target.name)
-                                if let until = target.until, until > Date() {
-                                    Spacer()
-                                    Text(until, style: .timer).foregroundColor(.loopGreen)
-                                }
-                            }
-                            Text(target.description).font(.caption2).foregroundColor(.secondary)
-                        }
-                    }
-                }
-            }
-
-            Button {
-                WKInterfaceDevice.current().play(.click)
-                state.enactTempTarget(id: "cancel")
-            } label: {
-                Text("Cancel Temp Target")
-            }
-        }
-        .navigationTitle("Temp Targets")
-    }
-}
-
-struct TempTargetsView_Previews: PreviewProvider {
-    static var previews: some View {
-        let model = WatchStateModel()
-        model.tempTargets = [
-            TempTargetWatchPreset(
-                name: "Target 0",
-                id: UUID().uuidString,
-                description: "blablabla",
-                until: Date().addingTimeInterval(60 * 60)
-            ),
-            TempTargetWatchPreset(name: "target1", id: UUID().uuidString, description: "blablabla", until: nil),
-            TempTargetWatchPreset(name: "🤖 Target 2", id: UUID().uuidString, description: "blablabla", until: nil)
-        ]
-        return TempTargetsView().environmentObject(model)
-    }
-}

+ 0 - 201
FreeAPSWatch WatchKit Extension/WatchStateModel.swift

@@ -1,201 +0,0 @@
-import Combine
-import Foundation
-import SwiftUI
-import WatchConnectivity
-
-enum AwConfig: String, CaseIterable, Identifiable, Codable {
-    var id: String { rawValue }
-    case HR
-    case BGTarget
-    case steps
-    case isf
-    case override
-}
-
-class WatchStateModel: NSObject, ObservableObject {
-    var session: WCSession
-
-    @Published var glucose = "00"
-    @Published var trend = "→"
-    @Published var delta = "+00"
-    @Published var lastLoopDate: Date?
-    @Published var glucoseDate: Date?
-    @Published var bolusIncrement: Decimal?
-    @Published var maxCOB: Decimal?
-    @Published var maxBolus: Decimal?
-    @Published var bolusRecommended: Decimal?
-    @Published var carbsRequired: Decimal?
-    @Published var iob: Decimal?
-    @Published var cob: Decimal?
-    @Published var tempTargets: [TempTargetWatchPreset] = []
-    @Published var isCarbsViewActive = false
-    @Published var isTempTargetViewActive = false
-    @Published var isBolusViewActive = false
-    @Published var displayOnWatch: AwConfig = .BGTarget
-    @Published var displayFatAndProteinOnWatch = false
-    @Published var confirmBolusFaster = false
-    @Published var eventualBG = ""
-    @Published var isConfirmationViewActive = false {
-        didSet {
-            confirmationTimeout = nil
-            if isConfirmationViewActive {
-                confirmationTimeout = Just(())
-                    .delay(for: 30, scheduler: DispatchQueue.main)
-                    .sink {
-                        WKInterfaceDevice.current().play(.retry)
-                        self.isConfirmationViewActive = false
-                    }
-            }
-        }
-    }
-
-    @Published var isConfirmationBolusViewActive = false
-    @Published var confirmationSuccess: Bool?
-    @Published var lastUpdate: Date = .distantPast
-    @Published var timerDate = Date()
-    @Published var pendingBolus: Double?
-    @Published var isf: Decimal?
-    @Published var override: String?
-
-    private var lifetime = Set<AnyCancellable>()
-    private var confirmationTimeout: AnyCancellable?
-    let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()
-
-    init(session: WCSession = .default) {
-        self.session = session
-        super.init()
-
-        session.delegate = self
-        session.activate()
-    }
-
-    func addMeal(_ carbs: Int, fat: Int, protein: Int, note: String) {
-        confirmationSuccess = nil
-        isConfirmationViewActive = true
-        isCarbsViewActive = false
-        session.sendMessage(["carbs": carbs, "fat": fat, "protein": protein, "note": note], replyHandler: { reply in
-            self.completionHandler(reply)
-            if let ok = reply["confirmation"] as? Bool, ok {
-                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
-                    self.isBolusViewActive = true
-                }
-            }
-        }) { error in
-            print(error.localizedDescription)
-            DispatchQueue.main.async {
-                self.confirmation(false)
-            }
-        }
-    }
-
-    func enactTempTarget(id: String) {
-        confirmationSuccess = nil
-        isConfirmationViewActive = true
-        isTempTargetViewActive = false
-        session.sendMessage(["tempTarget": id], replyHandler: completionHandler) { error in
-            print(error.localizedDescription)
-            DispatchQueue.main.async {
-                self.confirmation(false)
-            }
-        }
-    }
-
-    func addBolus(amount: Double) {
-        isBolusViewActive = false
-        pendingBolus = amount
-        isConfirmationBolusViewActive = true
-    }
-
-    func enactBolus() {
-        isConfirmationBolusViewActive = false
-        guard let amount = pendingBolus else { return }
-
-        confirmationSuccess = nil
-        isConfirmationViewActive = true
-        session.sendMessage(["bolus": amount], replyHandler: completionHandler) { error in
-            print(error.localizedDescription)
-            DispatchQueue.main.async {
-                self.confirmation(false)
-            }
-        }
-    }
-
-    func requestState() {
-        guard session.activationState == .activated else {
-            session.activate()
-            return
-        }
-        session.sendMessage(["stateRequest": true], replyHandler: nil) { error in
-            print("WatchStateModel error: " + error.localizedDescription)
-        }
-    }
-
-    private func completionHandler(_ reply: [String: Any]) {
-        if let ok = reply["confirmation"] as? Bool {
-            DispatchQueue.main.async {
-                self.confirmation(ok)
-            }
-        } else {
-            DispatchQueue.main.async {
-                self.confirmation(false)
-            }
-        }
-    }
-
-    private func confirmation(_ ok: Bool) {
-        WKInterfaceDevice.current().play(ok ? .success : .failure)
-        withAnimation {
-            confirmationSuccess = ok
-        }
-
-        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
-            withAnimation {
-                self.isConfirmationViewActive = false
-            }
-        }
-    }
-
-    private func processState(_ state: WatchState) {
-        glucose = state.glucose ?? "?"
-        trend = state.trend ?? "?"
-        delta = state.delta ?? "?"
-        glucoseDate = state.glucoseDate
-        lastLoopDate = state.lastLoopDate
-        bolusIncrement = state.bolusIncrement
-        maxCOB = state.maxCOB
-        maxBolus = state.maxBolus
-        bolusRecommended = state.bolusRecommended
-        carbsRequired = state.carbsRequired
-        iob = state.iob
-        cob = state.cob
-        tempTargets = state.tempTargets
-        lastUpdate = Date()
-        eventualBG = state.eventualBG ?? ""
-        displayOnWatch = state.displayOnWatch ?? .BGTarget
-        displayFatAndProteinOnWatch = state.displayFatAndProteinOnWatch ?? false
-        confirmBolusFaster = state.confirmBolusFaster ?? false
-        isf = state.isf
-        override = state.override
-    }
-}
-
-extension WatchStateModel: WCSessionDelegate {
-    func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
-        print("WCSession activated: \(state == .activated)")
-        requestState()
-    }
-
-    func session(_: WCSession, didReceiveMessage _: [String: Any]) {}
-
-    func sessionReachabilityDidChange(_ session: WCSession) {
-        print("WCSession Reachability: \(session.isReachable)")
-    }
-
-    func session(_: WCSession, didReceiveMessageData messageData: Data) {
-        if let state = try? JSONDecoder().decode(WatchState.self, from: messageData) {
-            DispatchQueue.main.async {
-                self.processState(state)
-            }
-        }
-    }
-}

+ 0 - 123
FreeAPSWatch/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -1,123 +0,0 @@
-{
-  "images" : [
-    {
-      "idiom" : "watch",
-      "role" : "notificationCenter",
-      "scale" : "2x",
-      "size" : "24x24",
-      "subtype" : "38mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "notificationCenter",
-      "scale" : "2x",
-      "size" : "27.5x27.5",
-      "subtype" : "42mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "companionSettings",
-      "scale" : "2x",
-      "size" : "29x29"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "companionSettings",
-      "scale" : "3x",
-      "size" : "29x29"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "notificationCenter",
-      "scale" : "2x",
-      "size" : "33x33",
-      "subtype" : "45mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "appLauncher",
-      "scale" : "2x",
-      "size" : "40x40",
-      "subtype" : "38mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "appLauncher",
-      "scale" : "2x",
-      "size" : "44x44",
-      "subtype" : "40mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "appLauncher",
-      "scale" : "2x",
-      "size" : "46x46",
-      "subtype" : "41mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "appLauncher",
-      "scale" : "2x",
-      "size" : "50x50",
-      "subtype" : "44mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "appLauncher",
-      "scale" : "2x",
-      "size" : "51x51",
-      "subtype" : "45mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "appLauncher",
-      "scale" : "2x",
-      "size" : "54x54",
-      "subtype" : "49mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "quickLook",
-      "scale" : "2x",
-      "size" : "86x86",
-      "subtype" : "38mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "quickLook",
-      "scale" : "2x",
-      "size" : "98x98",
-      "subtype" : "42mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "quickLook",
-      "scale" : "2x",
-      "size" : "108x108",
-      "subtype" : "44mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "quickLook",
-      "scale" : "2x",
-      "size" : "117x117",
-      "subtype" : "45mm"
-    },
-    {
-      "idiom" : "watch",
-      "role" : "quickLook",
-      "scale" : "2x",
-      "size" : "129x129",
-      "subtype" : "49mm"
-    },
-    {
-      "idiom" : "watch-marketing",
-      "scale" : "1x",
-      "size" : "1024x1024"
-    }
-  ],
-  "info" : {
-    "author" : "xcode",
-    "version" : 1
-  }
-}

+ 1 - 0
LiveActivity/LiveActivity.swift

@@ -84,6 +84,7 @@ private extension LiveActivityAttributes.ContentState {
         rotationDegrees: 0,
         cob: 20,
         iob: 1.5,
+        tdd: 43.21,
         isOverrideActive: false,
         overrideName: "Exercise",
         overrideDate: Date().addingTimeInterval(-3600),

+ 1 - 1
LiveActivity/Views/LiveActivityBGAndTrendView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityBGAndTrendView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //

+ 1 - 1
LiveActivity/Views/LiveActivityChartView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityChartView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //

+ 1 - 2
LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityGlucoseDeltaLabelView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //
@@ -11,7 +11,6 @@ import WidgetKit
 struct LiveActivityGlucoseDeltaLabelView: View {
     var context: ActivityViewContext<LiveActivityAttributes>
     var glucoseColor: Color
-    var isDetailed: Bool = false
 
     var body: some View {
         if !context.state.change.isEmpty {

+ 14 - 7
LiveActivity/Views/LiveActivityView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //
@@ -63,23 +63,31 @@ struct LiveActivityView: View {
                             case .currentGlucose:
                                 VStack {
                                     LiveActivityBGLabelView(context: context, additionalState: detailedViewState)
+
                                     HStack {
                                         LiveActivityGlucoseDeltaLabelView(
                                             context: context,
-                                            glucoseColor: .primary,
-                                            isDetailed: true
+                                            glucoseColor: .primary
                                         )
                                         if !context.isStale, let direction = context.state.direction {
                                             Text(direction).font(.headline)
                                         }
                                     }
                                 }
+                            case .currentGlucoseLarge:
+                                LiveActivityBGLabelLargeView(
+                                    context: context,
+                                    additionalState: detailedViewState,
+                                    glucoseColor: glucoseColor
+                                )
                             case .iob:
                                 LiveActivityIOBLabelView(context: context, additionalState: detailedViewState)
                             case .cob:
                                 LiveActivityCOBLabelView(context: context, additionalState: detailedViewState)
                             case .updatedLabel:
                                 LiveActivityUpdatedLabelView(context: context, isDetailedLayout: true)
+                            case .totalDailyDose:
+                                LiveActivityTotalDailyDoseView(context: context, additionalState: detailedViewState)
                             case .empty:
                                 Text("").frame(width: 50, height: 50)
                             }
@@ -120,8 +128,7 @@ struct LiveActivityView: View {
                         VStack(alignment: .trailing, spacing: 5) {
                             LiveActivityGlucoseDeltaLabelView(
                                 context: context,
-                                glucoseColor: hasStaticColorScheme ? .primary : glucoseColor,
-                                isDetailed: false
+                                glucoseColor: hasStaticColorScheme ? .primary : glucoseColor
                             ).font(.title3)
                             LiveActivityUpdatedLabelView(context: context, isDetailedLayout: false).font(.caption)
                                 .foregroundStyle(.primary.opacity(0.7))
@@ -158,7 +165,7 @@ struct LiveActivityExpandedTrailingView: View {
     var glucoseColor: Color
 
     var body: some View {
-        LiveActivityGlucoseDeltaLabelView(context: context, glucoseColor: glucoseColor, isDetailed: false).font(.title2)
+        LiveActivityGlucoseDeltaLabelView(context: context, glucoseColor: glucoseColor).font(.title2)
             .padding(.trailing, 5)
     }
 }
@@ -198,7 +205,7 @@ struct LiveActivityCompactTrailingView: View {
     var glucoseColor: Color
 
     var body: some View {
-        LiveActivityGlucoseDeltaLabelView(context: context, glucoseColor: glucoseColor, isDetailed: false).padding(.trailing, 4)
+        LiveActivityGlucoseDeltaLabelView(context: context, glucoseColor: glucoseColor).padding(.trailing, 4)
     }
 }
 

+ 32 - 0
LiveActivity/Views/WidgetItems/LiveActivityBGLabelLargeView.swift

@@ -0,0 +1,32 @@
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityBGLabelLargeView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+    var glucoseColor: Color
+
+    var body: some View {
+        HStack(alignment: .center) {
+            if let trendArrow = context.state.direction {
+                Text(context.state.bg)
+                    .fontWeight(.bold)
+                    .font(.title)
+                    .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+                Text(trendArrow)
+                    .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                    .fontWeight(.bold)
+                    .font(.headline)
+            } else {
+                Text(context.state.bg)
+                    .fontWeight(.bold)
+                    .font(.title)
+                    .foregroundStyle(context.isStale ? .secondary : glucoseColor)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            }
+        }
+    }
+}

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityBGLabelView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityCOBLabelView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //

+ 35 - 0
LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift

@@ -0,0 +1,35 @@
+import Foundation
+import SwiftUI
+import WidgetKit
+
+struct LiveActivityTotalDailyDoseView: View {
+    var context: ActivityViewContext<LiveActivityAttributes>
+    var additionalState: LiveActivityAttributes.ContentAdditionalState
+
+    private var bolusFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    var body: some View {
+        VStack(spacing: 2) {
+            HStack {
+                Text(
+                    bolusFormatter.string(from: additionalState.tdd as NSNumber) ?? "--"
+                )
+                .fontWeight(.bold)
+                .font(.title3)
+                .foregroundStyle(context.isStale ? .secondary : .primary)
+                .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+
+                Text("U")
+                    .font(.headline).fontWeight(.bold)
+                    .foregroundStyle(context.isStale ? .secondary : .primary)
+                    .strikethrough(context.isStale, pattern: .solid, color: .red.opacity(0.6))
+            }
+            Text("TDD").font(.subheadline).foregroundStyle(.primary)
+        }
+    }
+}

+ 1 - 1
LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift

@@ -1,6 +1,6 @@
 //
 //  LiveActivityUpdatedLabelView.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //

+ 4 - 0
Model/Classes+Properties/TempTargetRunStored+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(TempTargetRunStored) public class TempTargetRunStored: NSManagedObject {}

+ 18 - 0
Model/Classes+Properties/TempTargetRunStored+CoreDataProperties.swift

@@ -0,0 +1,18 @@
+import CoreData
+import Foundation
+
+public extension TempTargetRunStored {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<TempTargetRunStored> {
+        NSFetchRequest<TempTargetRunStored>(entityName: "TempTargetRunStored")
+    }
+
+    @NSManaged var startDate: Date?
+    @NSManaged var target: NSDecimalNumber?
+    @NSManaged var id: UUID?
+    @NSManaged var endDate: Date?
+    @NSManaged var isUploadedToNS: Bool
+    @NSManaged var tempTarget: TempTargetStored?
+    @NSManaged var name: String?
+}
+
+extension TempTargetRunStored: Identifiable {}

+ 4 - 0
Model/Classes+Properties/TempTargetStored+CoreDataClass.swift

@@ -0,0 +1,4 @@
+import CoreData
+import Foundation
+
+@objc(TempTargetStored) public class TempTargetStored: NSManagedObject {}

+ 22 - 0
Model/Classes+Properties/TempTargetStored+CoreDataProperties.swift

@@ -0,0 +1,22 @@
+import CoreData
+import Foundation
+
+public extension TempTargetStored {
+    @nonobjc class func fetchRequest() -> NSFetchRequest<TempTargetStored> {
+        NSFetchRequest<TempTargetStored>(entityName: "TempTargetStored")
+    }
+
+    @NSManaged var enabled: Bool
+    @NSManaged var date: Date?
+    @NSManaged var duration: NSDecimalNumber?
+    @NSManaged var target: NSDecimalNumber?
+    @NSManaged var id: UUID?
+    @NSManaged var name: String?
+    @NSManaged var isUploadedToNS: Bool
+    @NSManaged var isPreset: Bool
+    @NSManaged var halfBasalTarget: NSDecimalNumber?
+    @NSManaged var tempTargetRun: TempTargetRunStored?
+    @NSManaged var orderPosition: Int16
+}
+
+extension TempTargetStored: Identifiable {}

+ 19 - 9
Model/CoreDataObserver.swift

@@ -2,24 +2,34 @@ import Combine
 import CoreData
 import Foundation
 
-func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObject>, Never> {
+func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObjectID>, Never> {
     Foundation.NotificationCenter.default
         .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
         .map { notification in
-            guard let userInfo = notification.userInfo else { return Set<NSManagedObject>() }
+            guard let userInfo = notification.userInfo else { return Set<NSManagedObjectID>() }
 
-            var objects = Set((userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? [])
-            objects.formUnion((userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? [])
+            var objectIDs = Set<NSManagedObjectID>()
 
-            return objects
+            if let inserted = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(inserted.map(\.objectID))
+            }
+            if let updated = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(updated.map(\.objectID))
+            }
+            if let deleted = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject> {
+                objectIDs.formUnion(deleted.map(\.objectID))
+            }
+
+            return objectIDs
         }
 }
 
-extension Publisher where Output == Set<NSManagedObject> {
+extension Publisher where Output == Set<NSManagedObjectID> {
     func filterByEntityName(_ name: String) -> some Publisher<Self.Output, Self.Failure> {
-        filter { objects in
-            objects.contains(where: { $0.entity.name == name })
+        filter { objectIDs in
+            objectIDs.contains { objectID in
+                objectID.entity.name == name
+            }
         }
     }
 }

+ 1 - 0
Model/Helper/CustomNotification.swift

@@ -7,6 +7,7 @@ extension Notification.Name {
     static let willUpdateTempTargetConfiguration = Notification.Name("willUpdateTempTargetConfiguration")
     static let didUpdateTempTargetConfiguration = Notification.Name("didUpdateTempTargetConfiguration")
     static let liveActivityOrderDidChange = Notification.Name("liveActivityOrderDidChange")
+    static let openFromGarminConnect = Notification.Name("Notification.Name.openFromGarminConnect")
 }
 
 func awaitNotification(_ name: Notification.Name) async {

+ 2 - 2
Model/Helper/Determination+helper.swift

@@ -35,7 +35,7 @@ extension NSPredicate {
     static var enactedDeterminationsNotYetUploadedToNightscout: NSPredicate {
         NSPredicate(
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND enacted == %@",
-            Date.sixHoursAgo as NSDate,
+            Date.oneDayAgo as NSDate,
             false as NSNumber,
             true as NSNumber
         )
@@ -44,7 +44,7 @@ extension NSPredicate {
     static var suggestedDeterminationsNotYetUploadedToNightscout: NSPredicate {
         NSPredicate(
             format: "deliverAt >= %@ AND isUploadedToNS == %@ AND (enacted == %@ OR enacted == nil OR enacted != %@)",
-            Date.sixHoursAgo as NSDate,
+            Date.oneDayAgo as NSDate,
             false as NSNumber,
             true as NSNumber,
             true as NSNumber

+ 1 - 1
Model/Helper/TempTargetRunStored.swift

@@ -1,6 +1,6 @@
 //
 //  TempTargetRunStored.swift
-//  FreeAPS
+//  Trio
 //
 //  Created by Marvin Polscheit on 15.11.24.
 //

+ 4 - 4
TempTargetRunStored+CoreDataProperties.swift

@@ -6,13 +6,13 @@ public extension TempTargetRunStored {
         NSFetchRequest<TempTargetRunStored>(entityName: "TempTargetRunStored")
     }
 
-    @NSManaged var startDate: Date?
-    @NSManaged var target: NSDecimalNumber?
-    @NSManaged var id: UUID?
     @NSManaged var endDate: Date?
+    @NSManaged var id: UUID?
     @NSManaged var isUploadedToNS: Bool
-    @NSManaged var tempTarget: TempTargetStored?
     @NSManaged var name: String?
+    @NSManaged var startDate: Date?
+    @NSManaged var target: NSDecimalNumber?
+    @NSManaged var tempTarget: TempTargetStored?
 }
 
 extension TempTargetRunStored: Identifiable {}

+ 6 - 6
TempTargetStored+CoreDataProperties.swift

@@ -6,17 +6,17 @@ public extension TempTargetStored {
         NSFetchRequest<TempTargetStored>(entityName: "TempTargetStored")
     }
 
-    @NSManaged var enabled: Bool
     @NSManaged var date: Date?
     @NSManaged var duration: NSDecimalNumber?
-    @NSManaged var target: NSDecimalNumber?
+    @NSManaged var enabled: Bool
+    @NSManaged var halfBasalTarget: NSDecimalNumber?
     @NSManaged var id: UUID?
-    @NSManaged var name: String?
-    @NSManaged var isUploadedToNS: Bool
     @NSManaged var isPreset: Bool
-    @NSManaged var halfBasalTarget: NSDecimalNumber?
-    @NSManaged var tempTargetRun: TempTargetRunStored?
+    @NSManaged var isUploadedToNS: Bool
+    @NSManaged var name: String?
     @NSManaged var orderPosition: Int16
+    @NSManaged var target: NSDecimalNumber?
+    @NSManaged var tempTargetRun: TempTargetRunStored?
 }
 
 extension TempTargetStored: Identifiable {}

FreeAPS/Resources/Assets.xcassets/AccentColor.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/AccentColor.colorset/Contents.json


+ 14 - 0
Trio Watch App Extension/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,14 @@
+{
+  "images" : [
+    {
+      "filename" : "trioBlack watch.png",
+      "idiom" : "universal",
+      "platform" : "watchos",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

FreeAPS/Resources/Assets.xcassets/app_icons/trioBlack.appiconset/trioBlack watch.png → Trio Watch App Extension/Assets.xcassets/AppIcon.appiconset/trioBlack watch.png


FreeAPS/Resources/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/Basal.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/Chart.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/Chart.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/Insulin.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/LoopGray.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/LoopPink.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/LoopRed.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/LoopYellow.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/LoopYellow.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/TabBar.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/TempBasal.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/TempBasal.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/UAM.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Colors/ZT.colorset/Contents.json → Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json


FreeAPS/Resources/Assets.xcassets/Contents.json → Trio Watch App Extension/Assets.xcassets/Contents.json


+ 64 - 0
Trio Watch App Extension/Helper/Helper+ButtonStyles.swift

@@ -0,0 +1,64 @@
+import SwiftUI
+
+struct WatchOSButtonStyle: ButtonStyle {
+    let deviceType: WatchSize
+    var foregroundColor: Color = .white
+    var fontSize: Font = .title2
+
+    private var fontWeight: Font.Weight {
+        switch deviceType {
+        case .watch40mm:
+            return .medium
+        case .watch41mm:
+            return .medium
+        case .watch42mm:
+            return .medium
+        case .watch44mm:
+            return .semibold
+        case .watch45mm:
+            return .semibold
+        case .watch49mm:
+            return .bold
+        case .unknown:
+            return .semibold
+        }
+    }
+
+    private var buttonPadding: CGFloat {
+        switch deviceType {
+        case .watch40mm:
+            return 6
+        case .watch41mm:
+            return 6
+        case .watch42mm:
+            return 6
+        case .watch44mm:
+            return 8
+        case .watch45mm:
+            return 8
+        case .watch49mm:
+            return 8
+        case .unknown:
+            return 8
+        }
+    }
+
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .font(fontSize)
+            .fontWeight(fontWeight)
+            .padding(buttonPadding)
+            .background(Color.tabBar.opacity(configuration.isPressed ? 0.8 : 1.0))
+            .clipShape(Circle())
+            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
+    }
+}
+
+struct PressableIconButtonStyle: ButtonStyle {
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .background(Color.clear)
+            .opacity(configuration.isPressed ? 0.3 : 1.0) // Change opacity when pressed
+            .animation(.easeInOut(duration: 0.25), value: configuration.isPressed) // Smooth transition
+    }
+}

+ 56 - 0
Trio Watch App Extension/Helper/Helper+Enums.swift

@@ -0,0 +1,56 @@
+import SwiftUI
+import WatchKit
+
+enum NavigationDestinations: String {
+    case acknowledgmentPending = "AcknowledgmentPendingView"
+    case carbsInput = "CarbsInputView"
+    case bolusInput = "BolusInputView"
+    case bolusConfirm = "BolusConfirmView"
+}
+
+enum MealBolusStep: String {
+    case savingCarbs = "Saving Carbs..."
+    case enactingBolus = "Enacting Bolus..."
+}
+
+enum AcknowledgementStatus: String, CaseIterable {
+    case success
+    case failure
+    case pending
+}
+
+enum WatchSize {
+    case watch40mm
+    case watch41mm
+    case watch42mm
+    case watch44mm
+    case watch45mm
+    case watch49mm
+    case unknown
+
+    static var current: WatchSize {
+        let bounds = WKInterfaceDevice.current().screenBounds
+
+        switch bounds {
+        case CGRect(x: 0, y: 0, width: 162, height: 197):
+            return .watch40mm // check
+
+        case CGRect(x: 0, y: 0, width: 176, height: 215):
+            return .watch41mm // check
+
+        case CGRect(x: 0, y: 0, width: 187, height: 223):
+            return .watch42mm // check
+
+        case CGRect(x: 0, y: 0, width: 184, height: 224):
+            return .watch44mm
+
+        case CGRect(x: 0, y: 0, width: 198, height: 242):
+            return .watch45mm
+
+        case CGRect(x: 0, y: 0, width: 205, height: 251):
+            return .watch49mm
+        default:
+            return .unknown
+        }
+    }
+}

+ 32 - 0
Trio Watch App Extension/Helper/Helper+Extensions.swift

@@ -0,0 +1,32 @@
+import Foundation
+import SwiftUI
+
+extension Binding where Value == Int {
+    func doubleBinding() -> Binding<Double> {
+        Binding<Double>(
+            get: { Double(self.wrappedValue) },
+            set: { self.wrappedValue = Int($0) }
+        )
+    }
+}
+
+extension Color {
+    static let bgDarkBlue = Color("Background_DarkBlue")
+    static let bgDarkerDarkBlue = Color("Background_DarkerDarkBlue")
+}
+
+extension String {
+    func toColor() -> Color {
+        var hexString = trimmingCharacters(in: .whitespacesAndNewlines)
+        hexString = hexString.replacingOccurrences(of: "#", with: "")
+
+        var rgb: UInt64 = 0
+        Scanner(string: hexString).scanHexInt64(&rgb)
+
+        let red = Double((rgb & 0xFF0000) >> 16) / 255.0
+        let green = Double((rgb & 0x00FF00) >> 8) / 255.0
+        let blue = Double(rgb & 0x0000FF) / 255.0
+
+        return Color(red: red, green: green, blue: blue)
+    }
+}

FreeAPS/Resources/Assets.xcassets/app_icon_images/Contents.json → Trio Watch App Extension/Preview Content/Preview Assets.xcassets/Contents.json


+ 9 - 0
Trio Watch App Extension/TrioWatchApp.swift

@@ -0,0 +1,9 @@
+import SwiftUI
+
+@main struct TrioWatchApp: App {
+    var body: some Scene {
+        WindowGroup {
+            TrioMainWatchView()
+        }
+    }
+}

+ 56 - 0
Trio Watch App Extension/Views/AcknowledgementPendingView.swift

@@ -0,0 +1,56 @@
+import SwiftUI
+
+struct AcknowledgementPendingView: View {
+    @Binding var navigationPath: NavigationPath
+    let state: WatchState
+    @Binding var shouldNavigateToRoot: Bool
+
+    var trioBackgroundColor = LinearGradient(
+        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+        startPoint: .top,
+        endPoint: .bottom
+    )
+
+    var statusIcon: some View {
+        switch state.acknowledgementStatus {
+        case .pending:
+            return Image(systemName: "progress.indicator").foregroundStyle(Color.secondary)
+        case .success:
+            return Image(systemName: "checkmark.circle").foregroundStyle(Color.loopGreen)
+        case .failure:
+            return Image(systemName: "xmark").foregroundStyle(Color.loopRed)
+        }
+    }
+
+    var body: some View {
+        Group {
+            VStack {
+                if state.isMealBolusCombo {
+                    ProgressView()
+                    Text(state.mealBolusStep.rawValue).multilineTextAlignment(.center)
+                } else if state.showCommsAnimation {
+                    ProgressView()
+                    Text("Processing…")
+                } else if state.showAcknowledgmentBanner {
+                    statusIcon.padding()
+                    Text(state.acknowledgmentMessage).multilineTextAlignment(.center)
+                        .foregroundStyle(state.acknowledgementStatus == .failure ? Color.loopRed : Color.primary)
+                }
+            }
+            .padding()
+            .frame(maxWidth: .infinity, maxHeight: .infinity)
+        }
+        .navigationBarBackButtonHidden(true)
+        .toolbar(.hidden)
+        .background(trioBackgroundColor)
+        .onChange(of: state.showAcknowledgmentBanner) { _, newValue in
+            if !newValue {
+                // Navigate back to the root when acknowledgment banner disappears
+                navigationPath.removeLast(navigationPath.count)
+            }
+        }
+        .onDisappear {
+            state.shouldNavigateToRoot = true
+        }
+    }
+}

+ 123 - 0
Trio Watch App Extension/Views/BolusConfirmationView.swift

@@ -0,0 +1,123 @@
+import Foundation
+import SwiftUI
+import WatchKit
+
+struct BolusConfirmationView: View {
+    @Binding var navigationPath: NavigationPath
+    let state: WatchState
+    @Binding var bolusAmount: Double
+    @Binding var confirmationProgress: Double
+
+    @FocusState private var isCrownFocused: Bool
+
+    var trioBackgroundColor = LinearGradient(
+        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+        startPoint: .top,
+        endPoint: .bottom
+    )
+
+    var body: some View {
+        let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
+        let adjustedBolusAmount = floor(bolusAmount / bolusIncrement) * bolusIncrement
+
+        VStack(spacing: 10) {
+            Spacer()
+
+            VStack {
+                if state.carbsAmount > 0 {
+                    HStack {
+                        Text("Carbs:")
+                        Spacer()
+                        Text("\(state.carbsAmount) g")
+                            .bold()
+                            .foregroundStyle(.orange)
+                    }.padding(.horizontal)
+                }
+
+                HStack {
+                    Text("Bolus")
+                    Spacer()
+                    Text(String(format: "%.2f U", adjustedBolusAmount))
+                        .bold()
+                        .foregroundStyle(Color.insulin)
+                }.padding(.horizontal)
+            }
+
+            ProgressView(value: confirmationProgress, total: 1.0)
+                .tint(confirmationProgress >= 1.0 ? .loopGreen : .gray)
+                .padding(.horizontal)
+
+            Text("To confirm, dial crown.").font(.footnote)
+
+            Spacer()
+
+            Button("Cancel") {
+                if state.carbsAmount > 0 {
+                    state.carbsAmount = 0 // reset carbs in state
+                }
+                bolusAmount = 0 // reset bolus in state
+                confirmationProgress = 0 // reset auth progress
+                navigationPath.removeLast(navigationPath.count)
+            }
+            .buttonStyle(.bordered)
+        }
+        .focusable(true)
+        .focused($isCrownFocused)
+        .digitalCrownRotation(
+            $confirmationProgress,
+            from: 0.0,
+            through: 1.0,
+            by: state.confirmBolusFaster ? 0.5 : 0.05,
+            sensitivity: .medium,
+            isContinuous: false,
+            isHapticFeedbackEnabled: true
+        )
+        .onAppear {
+            isCrownFocused = true
+        }
+        .onChange(of: confirmationProgress) { _, newValue in
+            if newValue >= 1.0 {
+                WKInterfaceDevice.current().play(.success)
+
+                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+                    if state.carbsAmount > 0 {
+                        state.sendCarbsRequest(state.carbsAmount, Date())
+                        state.carbsAmount = 0 // reset carbs in state
+                    }
+                    state.activeBolusAmount = bolusAmount
+                    state.sendBolusRequest(Decimal(bolusAmount))
+                    bolusAmount = 0 // reset bolus in state
+                    confirmationProgress = 0 // reset auth progress
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }
+            } else if newValue > 0 {
+                WKInterfaceDevice.current().play(.click)
+            }
+        }
+        .navigationTitle("Confirm")
+        .background(trioBackgroundColor)
+        .toolbar {
+            ToolbarItem(placement: .topBarTrailing) {
+                Image(
+                    systemName: WKInterfaceDevice.current()
+                        .wristLocation == .left ? "digitalcrown.arrow.clockwise.fill" : "digitalcrown.arrow.counterclockwise.fill"
+                )
+                .symbolRenderingMode(.palette)
+                .foregroundStyle(Color.insulin, Color.primary)
+                .symbolEffect(
+                    .variableColor.reversing,
+                    options: .speed(100).repeating
+                )
+            }
+        }
+        .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
+        .overlay {
+            if state.showBolusProgressOverlay {
+                BolusProgressOverlay(state: state) {
+                    state.shouldNavigateToRoot = false
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }.transition(.opacity)
+            }
+        }
+    }
+}

+ 164 - 0
Trio Watch App Extension/Views/BolusInputView.swift

@@ -0,0 +1,164 @@
+import Foundation
+import SwiftUI
+import WatchKit
+
+// MARK: - Bolus Input View
+
+struct BolusInputView: View {
+    @Binding var navigationPath: NavigationPath
+    @State private var bolusAmount = 0.0
+
+    let state: WatchState
+
+    @FocusState private var isCrownFocused: Bool
+
+    private var effectiveBolusLimit: Double {
+        Double(truncating: state.maxBolus as NSNumber)
+    }
+
+    var trioBackgroundColor = LinearGradient(
+        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+        startPoint: .top,
+        endPoint: .bottom
+    )
+
+    var body: some View {
+        VStack {
+            if state.showBolusCalculationProgress {
+                ProgressView("Calculating Bolus...")
+                Spacer()
+            } else {
+                if effectiveBolusLimit <= 0 {
+                    VStack(spacing: 8) {
+                        Text("Bolus limit cannot be fetched from phone!").font(.headline)
+                        Text("Check device settings, connect to phone, and try again.").font(.caption)
+                    }
+                    .scenePadding()
+                } else {
+                    if state.carbsAmount > 0 {
+                        // Display the current carb amount
+                        HStack {
+                            Text("Carbs:").bold().font(.subheadline).padding(.leading)
+                            Text("\(state.carbsAmount) g").font(.subheadline).foregroundStyle(Color.orange)
+                            Spacer()
+                        }
+                    }
+
+                    Spacer()
+
+                    HStack {
+                        // "-" Button
+                        Button(action: {
+                            if bolusAmount > 0 { bolusAmount -= Double(truncating: state.bolusIncrement as NSNumber) }
+                        }) {
+                            Image(systemName: "minus.circle.fill")
+                                .font(.title3)
+                                .tint(Color.insulin)
+                        }
+                        .buttonStyle(.borderless)
+                        .disabled(bolusAmount <= 0)
+
+                        Spacer()
+
+                        let bolusIncrement = Double(truncating: state.bolusIncrement as NSNumber)
+                        let adjustedBolusAmount = floor(bolusAmount / bolusIncrement) * bolusIncrement
+
+                        Text(String(format: "%.2f U", adjustedBolusAmount))
+                            .fontWeight(.bold)
+                            .font(.system(.title2, design: .rounded))
+                            .foregroundColor(bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit ? .loopRed : .primary)
+                            .focusable(true)
+                            .focused($isCrownFocused)
+                            .digitalCrownRotation(
+                                $bolusAmount,
+                                from: 0,
+                                through: effectiveBolusLimit,
+                                by: Double(truncating: state.bolusIncrement as NSNumber),
+                                sensitivity: .medium,
+                                isContinuous: false,
+                                isHapticFeedbackEnabled: true
+                            )
+
+                        Spacer()
+
+                        // "+" Button
+                        Button(action: {
+                            bolusAmount = min(
+                                effectiveBolusLimit,
+                                bolusAmount + Double(truncating: state.bolusIncrement as NSNumber)
+                            )
+                        }) {
+                            Image(systemName: "plus.circle.fill")
+                                .font(.title3)
+                                .tint(Color.insulin)
+                        }
+                        .buttonStyle(.borderless)
+                        .disabled(bolusAmount >= effectiveBolusLimit)
+                    }.padding(.horizontal)
+
+                    Text("Insulin")
+                        .font(.subheadline)
+                        .foregroundColor(.secondary)
+                        .padding(.bottom)
+
+                    Spacer()
+
+                    if bolusAmount > 0.0 && bolusAmount >= effectiveBolusLimit {
+                        Text("Bolus Limit Reached!")
+                            .font(.footnote)
+                            .foregroundColor(.loopRed)
+                    }
+
+                    Button("Enact Bolus") {
+                        state.bolusAmount = min(bolusAmount, effectiveBolusLimit)
+                        navigationPath.append(NavigationDestinations.bolusConfirm)
+                    }
+                    .buttonStyle(.bordered)
+                    .tint(Color.insulin)
+                    .disabled(!(bolusAmount > 0.0) || bolusAmount > effectiveBolusLimit)
+
+                    Text(String(format: "Recommended: %.1f U", NSDecimalNumber(decimal: state.recommendedBolus).doubleValue))
+                        .font(.footnote)
+                        .foregroundStyle(.secondary)
+                }
+            }
+        }
+        .background(trioBackgroundColor)
+        .toolbar {
+            ToolbarItem(placement: .topBarTrailing) {
+                Image(systemName: "syringe.fill")
+                    .resizable()
+                    .aspectRatio(contentMode: .fit)
+                    .frame(width: 14, height: 14)
+                    .padding()
+                    .background(Color.insulin)
+                    .foregroundStyle(.white)
+                    .clipShape(Circle())
+            }
+        }
+        .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
+        .overlay {
+            if state.showBolusProgressOverlay {
+                BolusProgressOverlay(state: state) {
+                    state.shouldNavigateToRoot = false
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }.transition(.opacity)
+            }
+        }
+        .onAppear {
+            // Set initial bolus amount to recommended value
+            // Only do this if user has not updated amount previously, e.g., when navigating to next and then back to this view
+            if bolusAmount == 0 {
+                state.requestBolusRecommendation()
+                bolusAmount = Double(truncating: NSDecimalNumber(decimal: state.recommendedBolus))
+            }
+        }
+        // Add onChange to update bolus amount when recommendation changes
+        .onChange(of: state.recommendedBolus) { oldValue, newValue in
+            // Only update if user hasn't modified the value OR if recommendation hasn't changed
+            if bolusAmount == 0 || oldValue != newValue {
+                bolusAmount = Double(truncating: NSDecimalNumber(decimal: newValue))
+            }
+        }
+    }
+}

+ 63 - 0
Trio Watch App Extension/Views/BolusProgressOverlay.swift

@@ -0,0 +1,63 @@
+import SwiftUI
+
+struct BolusProgressOverlay: View {
+    let state: WatchState
+    let onCancelBolus: () -> Void
+
+    private let progressGradient = LinearGradient(
+        colors: [
+            Color(red: 0.7215686275, green: 0.3411764706, blue: 1), // #B857FF
+            Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569), // #9F6CFA
+            Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765), // #7C8BF3
+            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961), // #57AAEC
+            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902) // #43BBE9
+        ],
+        startPoint: .leading,
+        endPoint: .trailing
+    )
+
+    var body: some View {
+        VStack(spacing: 10) {
+            VStack {
+                Text("Bolusing")
+                    .font(.footnote)
+                    .foregroundStyle(.secondary)
+                    .padding(.top)
+
+                ProgressView(value: state.bolusProgress, total: 1.0)
+                    .tint(progressGradient)
+
+                Text(String(
+                    format: "%.2f U of %.2f U",
+                    state.deliveredAmount,
+                    state.activeBolusAmount
+                ))
+                    .font(.footnote)
+                    .foregroundStyle(.secondary)
+
+                Spacer()
+
+                Button(action: {
+                    state.sendCancelBolusRequest()
+                    onCancelBolus()
+                }) {
+                    Text("Cancel Bolus")
+                }
+                .buttonStyle(.bordered)
+                .padding()
+            }
+            .padding()
+            .background(Color.black.opacity(0.9))
+            .cornerRadius(10)
+        }
+        .scenePadding()
+        .onChange(of: state.bolusProgress) { _, newProgress in
+            if newProgress >= 1.0 {
+                state.activeBolusAmount = 0 // Reset only when bolus is complete
+            }
+        }
+        .onDisappear {
+            state.activeBolusAmount = 0 // Triple-check to reset when view disappears
+        }
+    }
+}

+ 118 - 0
Trio Watch App Extension/Views/CarbsInputView.swift

@@ -0,0 +1,118 @@
+import Foundation
+import SwiftUI
+
+// MARK: - Carbs Input View
+
+struct CarbsInputView: View {
+    @Binding var navigationPath: NavigationPath
+    @State private var carbsAmount: Double = 0.0 // Needs to be Double due to .digitalCrownRotation() stride
+    @FocusState private var isCrownFocused: Bool // Manage crown focus
+
+    let state: WatchState
+    let continueToBolus: Bool
+
+    private var effectiveCarbsLimit: Double {
+        Double(truncating: state.maxCarbs as NSNumber)
+    }
+
+    var trioBackgroundColor = LinearGradient(
+        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+        startPoint: .top,
+        endPoint: .bottom
+    )
+
+    var body: some View {
+        let buttonLabel = continueToBolus ? "Proceed" : "Log Carbs"
+
+        // TODO: introduce meal setting fpu enablement to conditional handle FPU
+        VStack {
+            Spacer()
+
+            HStack {
+                // "-" Button
+                Button(action: {
+                    if carbsAmount > 0 {
+                        carbsAmount < 5 ? carbsAmount = 0 : (carbsAmount -= 5)
+                    }
+                }) {
+                    Image(systemName: "minus.circle.fill")
+                        .font(.title3)
+                        .tint(.orange)
+                }
+                .buttonStyle(.borderless)
+                .disabled(carbsAmount <= 0)
+
+                Spacer()
+
+                // Display the current carb amount
+                Text(String(format: "%.0f g", carbsAmount))
+                    .fontWeight(.bold)
+                    .font(.system(.title2, design: .rounded))
+                    .foregroundColor(carbsAmount > 0.0 && carbsAmount >= effectiveCarbsLimit ? .loopRed : .primary)
+                    .focusable(true)
+                    .focused($isCrownFocused)
+                    .digitalCrownRotation(
+                        $carbsAmount,
+                        from: 0,
+                        through: effectiveCarbsLimit,
+                        by: 1,
+                        sensitivity: .medium,
+                        isContinuous: false,
+                        isHapticFeedbackEnabled: true
+                    )
+
+                Spacer()
+
+                // "+" Button
+                Button(action: {
+                    carbsAmount = min(effectiveCarbsLimit, carbsAmount + 5)
+                }) {
+                    Image(systemName: "plus.circle.fill")
+                        .font(.title3)
+                        .tint(.orange)
+                }
+                .buttonStyle(.borderless)
+                .disabled(carbsAmount >= effectiveCarbsLimit)
+            }.padding(.horizontal)
+
+            Text("Carbohydrates")
+                .font(.subheadline)
+                .foregroundColor(.secondary)
+                .padding(.bottom)
+
+            Spacer()
+
+            if carbsAmount > 0.0 && carbsAmount >= effectiveCarbsLimit {
+                Text("Carbs Limit Reached!")
+                    .font(.footnote)
+                    .foregroundColor(.loopRed)
+            }
+
+            Button(buttonLabel) {
+                if continueToBolus {
+                    state.carbsAmount = Int(min(carbsAmount, effectiveCarbsLimit))
+                    navigationPath.append(NavigationDestinations.bolusInput)
+                } else {
+                    state.sendCarbsRequest(Int(min(carbsAmount, effectiveCarbsLimit)))
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }
+            }
+            .buttonStyle(.bordered)
+            .tint(.orange)
+            .disabled(!(carbsAmount > 0.0) || carbsAmount > effectiveCarbsLimit)
+        }
+        .background(trioBackgroundColor)
+        .toolbar {
+            ToolbarItem(placement: .topBarTrailing) {
+                Image(systemName: "fork.knife")
+                    .resizable()
+                    .aspectRatio(contentMode: .fit)
+                    .frame(width: 14, height: 14)
+                    .padding()
+                    .background(Color.orange)
+                    .foregroundStyle(.white)
+                    .clipShape(Circle())
+            }
+        }
+    }
+}

+ 90 - 0
Trio Watch App Extension/Views/GlucoseChartView.swift

@@ -0,0 +1,90 @@
+import Charts
+import Foundation
+import SwiftUI
+
+// MARK: - Current Glucose View
+
+struct GlucoseChartView: View {
+    let glucoseValues: [(date: Date, glucose: Double, color: Color)]
+    @State private var timeWindow: TimeWindow = .threeHours
+
+    enum TimeWindow: Int {
+        case threeHours = 3
+        case sixHours = 6
+        case twelveHours = 12
+        case twentyFourHours = 24
+
+        var next: TimeWindow {
+            switch self {
+            case .threeHours: return .sixHours
+            case .sixHours: return .twelveHours
+            case .twelveHours: return .twentyFourHours
+            case .twentyFourHours: return .threeHours
+            }
+        }
+    }
+
+    // TODO: should we only change the x axis here like we do in the main chart instead of filtering the values?
+    private var filteredValues: [(date: Date, glucose: Double, color: Color)] {
+        let cutoffDate = Date().addingTimeInterval(-Double(timeWindow.rawValue) * 3600)
+        return glucoseValues.filter { $0.date > cutoffDate }
+    }
+
+    var glucosePointSize: CGFloat {
+        switch timeWindow {
+        case .threeHours: return 18
+        case .sixHours: return 14
+        case .twelveHours: return 10
+        case .twentyFourHours: return 6
+        }
+    }
+
+    var body: some View {
+        VStack(spacing: 8) {
+            if filteredValues.isEmpty {
+                Text("No glucose readings.").font(.headline)
+                Text("Check phone and CGM connectivity.").font(.caption)
+            } else {
+                Chart {
+                    ForEach(filteredValues, id: \.date) { reading in
+                        PointMark(
+                            x: .value("Time", reading.date),
+                            y: .value("Glucose", reading.glucose)
+                        )
+                        .foregroundStyle(reading.color)
+                        .symbolSize(glucosePointSize)
+                    }
+                }
+                .chartXAxis(.hidden)
+                .chartYAxisLabel("\(timeWindow.rawValue) h", alignment: .topLeading)
+                .chartYAxis {
+                    AxisMarks(position: .trailing) { value in
+                        AxisGridLine(stroke: .init(lineWidth: 0.65, dash: [2, 3]))
+                            .foregroundStyle(Color.white.opacity(0.25))
+
+                        AxisValueLabel {
+                            if let glucose = value.as(Double.self) {
+                                Text("\(Int(glucose))")
+                            }
+                        }
+                    }
+                }
+                .chartPlotStyle { plotContent in
+                    plotContent
+                        .background(
+                            RoundedRectangle(cornerRadius: 12)
+                                .fill(Color.clear)
+                        )
+                        .clipShape(RoundedRectangle(cornerRadius: 12))
+                }
+                .padding(.bottom)
+            }
+        }
+        .scenePadding()
+        .onTapGesture {
+            withAnimation {
+                timeWindow = timeWindow.next
+            }
+        }
+    }
+}

+ 159 - 0
Trio Watch App Extension/Views/GlucoseTrendView.swift

@@ -0,0 +1,159 @@
+import SwiftUI
+
+struct GlucoseTrendView: View {
+    let state: WatchState
+    let rotationDegrees: Double
+    let isWatchStateDated: Bool
+
+    /// Determines the status color based on the time elapsed since the last loop
+    /// - Parameter timeString: The time string representing minutes since last loop (format: "X min")
+    /// - Returns: A color indicating the status:
+    ///   - Green: <= 5 minutes
+    ///   - Yellow: 5-10 minutes
+    ///   - Red: > 10 minutes or invalid time
+    private func statusColor(for timeString: String?) -> Color {
+        guard let timeString = timeString,
+              timeString != "--",
+              let minutes = timeString.split(separator: " ").first.flatMap({ Int($0) })
+        else {
+            return Color.secondary
+        }
+
+        guard !isWatchStateDated else {
+            return Color.secondary
+        }
+
+        switch minutes {
+        case ...5:
+            return Color.loopGreen
+        case 5 ... 10:
+            return Color.loopYellow
+        case 11...:
+            return Color.loopRed
+        default:
+            return Color.secondary
+        }
+    }
+
+    var circleSize: CGFloat {
+        switch state.deviceType {
+        case .watch40mm:
+            return 82
+        case .watch41mm,
+             .watch42mm:
+            return 86
+        case .watch44mm:
+            return 96
+        case .unknown,
+             .watch45mm:
+            return 103
+        case .watch49mm:
+            return 105
+        }
+    }
+
+    var lineWidth: CGFloat {
+        switch state.deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm,
+             .watch44mm:
+            return 1
+        case .unknown,
+             .watch45mm:
+            return 1.5
+        case .watch49mm:
+            return 1.5
+        }
+    }
+
+    var shadowRadius: CGFloat {
+        switch state.deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm:
+            return 8
+        case .watch44mm:
+            return 9
+        case .unknown,
+             .watch45mm:
+            return 12
+        case .watch49mm:
+            return 12
+        }
+    }
+
+    var currentGlucoseFontSize: Font {
+        switch state.deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm,
+             .watch44mm:
+            return .title2
+        case .unknown,
+             .watch45mm:
+            return .title
+        case .watch49mm:
+            return .title
+        }
+    }
+
+    var minutesAgoFontSize: CGFloat {
+        switch state.deviceType {
+        case .watch40mm,
+             .watch41mm:
+            return 9
+        case .unknown,
+             .watch42mm,
+             .watch44mm:
+            return 10
+        case .watch45mm:
+            return 11
+        case .watch49mm:
+            return 10
+        }
+    }
+
+    var body: some View {
+        VStack {
+            ZStack {
+                Circle()
+                    .stroke(statusColor(for: state.lastLoopTime), lineWidth: lineWidth)
+                    .frame(width: circleSize, height: circleSize)
+                    .background(Circle().fill(Color.bgDarkBlue))
+                    .shadow(color: statusColor(for: state.lastLoopTime), radius: shadowRadius)
+
+                TrendShape(
+                    isWatchStateDated: isWatchStateDated,
+                    rotationDegrees: rotationDegrees,
+                    deviceType: state.deviceType
+                )
+                .animation(.spring(response: 0.5, dampingFraction: 0.6), value: rotationDegrees)
+                .shadow(color: Color.black.opacity(0.5), radius: 5)
+
+                VStack(alignment: .center) {
+                    Text(isWatchStateDated ? "--" : state.currentGlucose)
+                        .fontWeight(.semibold)
+                        .font(currentGlucoseFontSize)
+                        .foregroundStyle(isWatchStateDated ? Color.secondary : state.currentGlucoseColorString.toColor())
+
+                    if let delta = state.delta {
+                        Text(isWatchStateDated ? "--" : delta)
+                            .fontWeight(.semibold)
+                            .font(.system(.caption))
+                            .foregroundStyle(.secondary)
+                    }
+                }
+            }
+
+            Spacer()
+
+            Text(isWatchStateDated ? "STALE DATA" : state.lastLoopTime ?? "--")
+                .font(.system(size: minutesAgoFontSize))
+                .fontWidth(isWatchStateDated ? .expanded : .standard)
+
+            Spacer()
+
+        }.frame(maxWidth: .infinity, maxHeight: .infinity)
+    }
+}

+ 77 - 0
Trio Watch App Extension/Views/OverridePresetsView.swift

@@ -0,0 +1,77 @@
+import SwiftUI
+
+struct OverridePresetsView: View {
+    let state: WatchState
+    let overridePresets: [OverridePresetWatch]
+    var onPresetAction: () -> Void // Callback to handle selection of preset, or cancellation, and dismiss the sheet
+
+    private let activePresetGradient = LinearGradient(
+        colors: [
+            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902), // #43BBE9
+            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961) // #57AAEC
+        ],
+        startPoint: .leading,
+        endPoint: .trailing
+    )
+
+    private var sortedPresets: [OverridePresetWatch] {
+        overridePresets.sorted { $0.isEnabled && !$1.isEnabled }
+    }
+
+    private var activeOverride: OverridePresetWatch? {
+        sortedPresets.first { $0.isEnabled }
+    }
+
+    var body: some View {
+        NavigationView {
+            List {
+                if let active = activeOverride {
+                    Button("Stop \(active.name)") {
+                        state.sendCancelOverrideRequest()
+                        onPresetAction()
+                    }
+                    .foregroundColor(.white)
+                    .listRowBackground(
+                        Color.loopRed
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
+                    )
+                }
+
+                if sortedPresets.isEmpty {
+                    Text("No Override Presets")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                } else {
+                    ForEach(sortedPresets, id: \.name) { preset in
+                        Button(action: {
+                            if !preset.isEnabled {
+                                state.sendActivateOverrideRequest(presetName: preset.name)
+                            }
+                            onPresetAction()
+                        }) {
+                            HStack {
+                                Text(preset.name)
+                                    .font(.caption)
+
+                                if preset.isEnabled {
+                                    Spacer()
+                                    Text("is running")
+                                        .font(.caption2)
+                                        .foregroundStyle(.white)
+                                }
+                            }
+                        }
+                        .listRowBackground(
+                            preset.isEnabled ?
+                                activePresetGradient
+                                .clipShape(RoundedRectangle(cornerRadius: 8))
+                                : nil
+                        )
+                        .foregroundColor(preset.isEnabled ? .white : .primary)
+                    }
+                }
+            }
+            .navigationTitle("Override Presets")
+        }
+    }
+}

+ 77 - 0
Trio Watch App Extension/Views/TempTargetPresetsView.swift

@@ -0,0 +1,77 @@
+import SwiftUI
+
+struct TempTargetPresetsView: View {
+    let state: WatchState
+    let tempTargetPresets: [TempTargetPresetWatch]
+    var onPresetAction: () -> Void // Callback to handle selection of preset, or cancellation, and dismiss the sheet
+
+    private let activePresetGradient = LinearGradient(
+        colors: [
+            Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902), // #43BBE9
+            Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961) // #57AAEC
+        ],
+        startPoint: .leading,
+        endPoint: .trailing
+    )
+
+    private var sortedPresets: [TempTargetPresetWatch] {
+        tempTargetPresets.sorted { $0.isEnabled && !$1.isEnabled }
+    }
+
+    private var activePreset: TempTargetPresetWatch? {
+        sortedPresets.first { $0.isEnabled }
+    }
+
+    var body: some View {
+        NavigationView {
+            List {
+                if let active = activePreset {
+                    Button("Stop \(active.name)") {
+                        state.sendCancelTempTargetRequest()
+                        onPresetAction()
+                    }
+                    .foregroundColor(.white)
+                    .listRowBackground(
+                        Color.loopRed
+                            .clipShape(RoundedRectangle(cornerRadius: 8))
+                    )
+                }
+
+                if sortedPresets.isEmpty {
+                    Text("No Temp Target Presets")
+                        .font(.caption)
+                        .foregroundColor(.secondary)
+                } else {
+                    ForEach(sortedPresets, id: \.name) { preset in
+                        Button(action: {
+                            if !preset.isEnabled {
+                                state.sendActivateTempTargetRequest(presetName: preset.name)
+                            }
+                            onPresetAction()
+                        }) {
+                            HStack {
+                                Text(preset.name)
+                                    .font(.caption)
+
+                                if preset.isEnabled {
+                                    Spacer()
+                                    Text("is running")
+                                        .font(.caption2)
+                                        .foregroundStyle(.white)
+                                }
+                            }
+                        }
+                        .listRowBackground(
+                            preset.isEnabled ?
+                                activePresetGradient
+                                .clipShape(RoundedRectangle(cornerRadius: 8))
+                                : nil
+                        )
+                        .foregroundColor(preset.isEnabled ? .white : .primary)
+                    }
+                }
+            }
+            .navigationTitle("Temp Target Presets")
+        }
+    }
+}

+ 0 - 0
Trio Watch App Extension/Views/TreatmentMenuView.swift


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels