فهرست منبع

Sync oref-swift with latest TCD to pull in renaming

Deniz Cengiz 1 سال پیش
والد
کامیت
9cd417525a
100فایلهای تغییر یافته به همراه2539 افزوده شده و 2940 حذف شده
  1. 1 1
      DanaKit
  2. 0 2
      FreeAPS/Resources/javascript/bundle/profile.js
  3. 0 16
      FreeAPS/Sources/APS/OpenAPS/Script.swift
  4. 0 22
      FreeAPS/Sources/Models/GlucoseColorScheme.swift
  5. 0 137
      FreeAPS/Sources/Modules/WatchConfig/View/WatchConfigAppleWatchView.swift
  6. 0 78
      FreeAPS/Sources/Modules/WatchConfig/WatchConfigStateModel.swift
  7. 0 213
      FreeAPS/Sources/Services/WatchManager/GarminManager.swift
  8. 0 537
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  9. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/36x36 Circular.png
  10. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
  11. 0 53
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json
  12. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/203x203 Square.png
  13. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
  14. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/84x84 Square.png
  15. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
  16. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/84x84 Circular.png
  17. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
  18. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/40x40 Square.png
  19. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
  20. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/240x240 Square.png
  21. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json
  22. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/300x94.png
  23. 0 21
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
  24. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/58x58 Square.png
  25. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
  26. BIN
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/44x44 Square.png
  27. 0 26
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
  28. 0 106
      FreeAPSWatch WatchKit Extension/ComplicationController.swift
  29. 0 33
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  30. 0 15
      FreeAPSWatch WatchKit Extension/FreeAPSApp.swift
  31. 0 16
      FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements
  32. 0 22
      FreeAPSWatch WatchKit Extension/Info.plist
  33. 0 25
      FreeAPSWatch WatchKit Extension/NotificationController.swift
  34. 0 13
      FreeAPSWatch WatchKit Extension/NotificationView.swift
  35. 0 20
      FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns
  36. 0 118
      FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift
  37. 0 100
      FreeAPSWatch WatchKit Extension/Views/BolusView.swift
  38. 0 253
      FreeAPSWatch WatchKit Extension/Views/CarbsView.swift
  39. 0 86
      FreeAPSWatch WatchKit Extension/Views/ConfirmationView.swift
  40. 0 444
      FreeAPSWatch WatchKit Extension/Views/MainView.swift
  41. 0 56
      FreeAPSWatch WatchKit Extension/Views/TempTargetsView.swift
  42. 0 201
      FreeAPSWatch WatchKit Extension/WatchStateModel.swift
  43. 0 123
      FreeAPSWatch/Assets.xcassets/AppIcon.appiconset/Contents.json
  44. 1 0
      LiveActivity/LiveActivity.swift
  45. 1 1
      LiveActivity/Views/LiveActivityBGAndTrendView.swift
  46. 1 1
      LiveActivity/Views/LiveActivityChartView.swift
  47. 1 2
      LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift
  48. 14 7
      LiveActivity/Views/LiveActivityView.swift
  49. 32 0
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelLargeView.swift
  50. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityBGLabelView.swift
  51. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityCOBLabelView.swift
  52. 35 0
      LiveActivity/Views/WidgetItems/LiveActivityTotalDailyDoseView.swift
  53. 1 1
      LiveActivity/Views/WidgetItems/LiveActivityUpdatedLabelView.swift
  54. 0 0
      Model/Classes+Properties/TempTargetRunStored+CoreDataClass.swift
  55. 4 4
      Model/Classes+Properties/TempTargetRunStored+CoreDataProperties.swift
  56. 0 0
      Model/Classes+Properties/TempTargetStored+CoreDataClass.swift
  57. 6 6
      Model/Classes+Properties/TempTargetStored+CoreDataProperties.swift
  58. 19 9
      Model/CoreDataObserver.swift
  59. 1 0
      Model/Helper/CustomNotification.swift
  60. 2 2
      Model/Helper/Determination+helper.swift
  61. 1 1
      Model/Helper/TempTargetRunStored.swift
  62. 0 0
      Trio Watch App Extension/Assets.xcassets/AccentColor.colorset/Contents.json
  63. 14 0
      Trio Watch App Extension/Assets.xcassets/AppIcon.appiconset/Contents.json
  64. 0 0
      Trio Watch App Extension/Assets.xcassets/AppIcon.appiconset/trioBlack watch.png
  65. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkBlue.colorset/Contents.json
  66. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Background_DarkerDarkBlue.colorset/Contents.json
  67. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Basal.colorset/Contents.json
  68. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Chart.colorset/Contents.json
  69. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Contents.json
  70. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/Insulin.colorset/Contents.json
  71. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGray.colorset/Contents.json
  72. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopGreen.colorset/Contents.json
  73. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopPink.colorset/Contents.json
  74. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopRed.colorset/Contents.json
  75. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/LoopYellow.colorset/Contents.json
  76. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/TabBar.colorset/Contents.json
  77. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/TempBasal.colorset/Contents.json
  78. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/UAM.colorset/Contents.json
  79. 0 0
      Trio Watch App Extension/Assets.xcassets/Colors/ZT.colorset/Contents.json
  80. 0 0
      Trio Watch App Extension/Assets.xcassets/Contents.json
  81. 64 0
      Trio Watch App Extension/Helper/Helper+ButtonStyles.swift
  82. 56 0
      Trio Watch App Extension/Helper/Helper+Enums.swift
  83. 32 0
      Trio Watch App Extension/Helper/Helper+Extensions.swift
  84. 0 0
      Trio Watch App Extension/Preview Content/Preview Assets.xcassets/Contents.json
  85. 9 0
      Trio Watch App Extension/TrioWatchApp.swift
  86. 56 0
      Trio Watch App Extension/Views/AcknowledgementPendingView.swift
  87. 123 0
      Trio Watch App Extension/Views/BolusConfirmationView.swift
  88. 164 0
      Trio Watch App Extension/Views/BolusInputView.swift
  89. 63 0
      Trio Watch App Extension/Views/BolusProgressOverlay.swift
  90. 118 0
      Trio Watch App Extension/Views/CarbsInputView.swift
  91. 90 0
      Trio Watch App Extension/Views/GlucoseChartView.swift
  92. 159 0
      Trio Watch App Extension/Views/GlucoseTrendView.swift
  93. 77 0
      Trio Watch App Extension/Views/OverridePresetsView.swift
  94. 77 0
      Trio Watch App Extension/Views/TempTargetPresetsView.swift
  95. 111 0
      Trio Watch App Extension/Views/TreatmentMenuView.swift
  96. 175 0
      Trio Watch App Extension/Views/TrendShape.swift
  97. 271 0
      Trio Watch App Extension/Views/TrioMainWatchView.swift
  98. 170 0
      Trio Watch App Extension/WatchState+Requests.swift
  99. 588 0
      Trio Watch App Extension/WatchState.swift
  100. 0 0
      Trio Watch App Tests/Unit Tests.swift

+ 1 - 1
DanaKit

@@ -1 +1 @@
-Subproject commit df0081b6db9e70818638954941916206ecbf6ece
+Subproject commit b07f236677b205d31d7ecf6144970348e8d5a3fe

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 2
FreeAPS/Resources/javascript/bundle/profile.js


+ 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,
         rotationDegrees: 0,
         cob: 20,
         cob: 20,
         iob: 1.5,
         iob: 1.5,
+        tdd: 43.21,
         isOverrideActive: false,
         isOverrideActive: false,
         overrideName: "Exercise",
         overrideName: "Exercise",
         overrideDate: Date().addingTimeInterval(-3600),
         overrideDate: Date().addingTimeInterval(-3600),

+ 1 - 1
LiveActivity/Views/LiveActivityBGAndTrendView.swift

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

+ 1 - 1
LiveActivity/Views/LiveActivityChartView.swift

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

+ 1 - 2
LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift

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

+ 14 - 7
LiveActivity/Views/LiveActivityView.swift

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

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

@@ -1,6 +1,6 @@
 //
 //
 //  LiveActivityCOBLabelView.swift
 //  LiveActivityCOBLabelView.swift
-//  FreeAPS
+//  Trio
 //
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //  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
 //  LiveActivityUpdatedLabelView.swift
-//  FreeAPS
+//  Trio
 //
 //
 //  Created by Cengiz Deniz on 17.10.24.
 //  Created by Cengiz Deniz on 17.10.24.
 //
 //

TempTargetRunStored+CoreDataClass.swift → Model/Classes+Properties/TempTargetRunStored+CoreDataClass.swift


TempTargetRunStored+CoreDataProperties.swift → Model/Classes+Properties/TempTargetRunStored+CoreDataProperties.swift


TempTargetStored+CoreDataClass.swift → Model/Classes+Properties/TempTargetStored+CoreDataClass.swift


TempTargetStored+CoreDataProperties.swift → Model/Classes+Properties/TempTargetStored+CoreDataProperties.swift


+ 19 - 9
Model/CoreDataObserver.swift

@@ -2,24 +2,34 @@ import Combine
 import CoreData
 import CoreData
 import Foundation
 import Foundation
 
 
-func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObject>, Never> {
+func changedObjectsOnManagedObjectContextDidSavePublisher() -> some Publisher<Set<NSManagedObjectID>, Never> {
     Foundation.NotificationCenter.default
     Foundation.NotificationCenter.default
         .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
         .publisher(for: NSNotification.Name.NSManagedObjectContextDidSave)
         .map { notification in
         .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> {
     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 willUpdateTempTargetConfiguration = Notification.Name("willUpdateTempTargetConfiguration")
     static let didUpdateTempTargetConfiguration = Notification.Name("didUpdateTempTargetConfiguration")
     static let didUpdateTempTargetConfiguration = Notification.Name("didUpdateTempTargetConfiguration")
     static let liveActivityOrderDidChange = Notification.Name("liveActivityOrderDidChange")
     static let liveActivityOrderDidChange = Notification.Name("liveActivityOrderDidChange")
+    static let openFromGarminConnect = Notification.Name("Notification.Name.openFromGarminConnect")
 }
 }
 
 
 func awaitNotification(_ name: Notification.Name) async {
 func awaitNotification(_ name: Notification.Name) async {

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

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

+ 1 - 1
Model/Helper/TempTargetRunStored.swift

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

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

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

@@ -0,0 +1,111 @@
+import SwiftUI
+
+struct TreatmentMenuView: View {
+    @Environment(\.dismiss) var dismiss
+    let deviceType: WatchSize
+    @Binding var selectedTreatment: TreatmentOption?
+    var onSelect: () -> Void // Callback to handle selection and dismiss the sheet
+
+    // Define in array to achieve custom order of treatment options
+    let treatments: [TreatmentOption] = [
+        .meal, // First
+        .bolus, // Second
+        .mealBolusCombo // Third
+    ]
+
+    private var iconSize: CGFloat {
+        switch deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm:
+            return 18
+        case .unknown,
+             .watch44mm,
+             .watch45mm:
+            return 22
+        case .watch49mm:
+            return 24
+        }
+    }
+
+    private var iconPadding: CGFloat {
+        switch deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm:
+            return 6
+        case .unknown,
+             .watch44mm,
+             .watch45mm:
+            return 10
+        case .watch49mm:
+            return 12
+        }
+    }
+
+    var body: some View {
+        VStack {
+            List {
+                ForEach(treatments) { treatment in
+                    Button(action: {
+                        selectedTreatment = treatment
+                        onSelect()
+                    }) {
+                        HStack(spacing: 10) {
+                            switch treatment {
+                            case .meal:
+                                mealIcon
+                                Text(treatment.displayName)
+                            case .bolus:
+                                bolusIcon
+                                Text(treatment.displayName)
+                            case .mealBolusCombo:
+                                mealIcon
+                                bolusIcon
+                            }
+                        }
+                        .foregroundColor(.white)
+                        .frame(maxWidth: .infinity)
+                    }
+                    .buttonStyle(PressableIconButtonStyle())
+                }
+            }.navigationTitle("Pick Treatment")
+        }
+    }
+
+    var mealIcon: some View {
+        Image(systemName: "fork.knife")
+            .resizable()
+            .aspectRatio(contentMode: .fit)
+            .frame(width: iconSize, height: iconSize)
+            .padding(iconPadding)
+            .background(Color.orange)
+            .clipShape(Circle())
+    }
+
+    var bolusIcon: some View {
+        Image(systemName: "syringe.fill")
+            .resizable()
+            .aspectRatio(contentMode: .fit)
+            .frame(width: iconSize, height: iconSize)
+            .padding(iconPadding)
+            .background(Color.insulin)
+            .clipShape(Circle())
+    }
+}
+
+enum TreatmentOption: String, CaseIterable, Identifiable {
+    var id: String { rawValue }
+
+    case mealBolusCombo
+    case meal
+    case bolus
+
+    var displayName: String {
+        switch self {
+        case .mealBolusCombo: return "Meal & Bolus"
+        case .meal: return "Meal"
+        case .bolus: return "Bolus"
+        }
+    }
+}

+ 175 - 0
Trio Watch App Extension/Views/TrendShape.swift

@@ -0,0 +1,175 @@
+import SwiftUI
+
+struct Triangle: Shape {
+    /// Flag to be able to adjust size based on Apple Watch size
+    let deviceType: WatchSize
+
+    private var triangleTipFactor: CGFloat {
+        switch deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm:
+            return 7.5
+        case .unknown,
+             .watch44mm,
+             .watch45mm:
+            return 9
+        case .watch49mm:
+            return 9
+        }
+    }
+
+    private var triangleBezierFactor: CGFloat {
+        switch deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm:
+            return 5
+        case .unknown,
+             .watch44mm,
+             .watch45mm:
+            return 7
+        case .watch49mm:
+            return 7
+        }
+    }
+
+    /// Creates a triangle shape pointing to the right
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+
+        // Draw the triangle pointing to the right
+        path.move(to: CGPoint(x: rect.maxX - triangleTipFactor, y: rect.midY))
+        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
+        path.addQuadCurve(
+            to: CGPoint(x: rect.minX, y: rect.maxY),
+            control: CGPoint(x: rect.midX - triangleBezierFactor, y: rect.midY)
+        )
+        path.closeSubpath()
+
+        return path
+    }
+}
+
+/// A view that displays a circular trend indicator with a directional triangle
+struct TrendShape: View {
+    let isWatchStateDated: Bool
+    /// Rotation angle in degrees for the trend direction
+    let rotationDegrees: Double
+    /// Flag to be able to adjust size based on Apple Watch size
+    let deviceType: WatchSize
+
+    // Angular gradient for the outer circle, transitioning through various blues and purples
+    private let angularGradient = AngularGradient(
+        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
+            Color(red: 0.7215686275, green: 0.3411764706, blue: 1) // #B857FF (repeated for seamless transition)
+        ],
+        center: .center,
+        startAngle: .degrees(270),
+        endAngle: .degrees(-90)
+    )
+
+    private let staleWatchStateGradient = AngularGradient(
+        colors: [
+            Color.secondary,
+            Color.secondary.opacity(0.8),
+            Color.secondary.opacity(0.6),
+            Color.secondary.opacity(0.4),
+            Color.secondary
+        ],
+        center: .center
+    )
+
+    // Color for the direction indicator triangle
+    private let triangleColor = Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902) // #43BBE9
+
+    private var strokeWidth: CGFloat {
+        switch deviceType {
+        case .watch40mm:
+            return 3
+        case .watch41mm,
+             .watch42mm:
+            return 4
+        case .unknown,
+             .watch44mm,
+             .watch45mm:
+            return 4
+        case .watch49mm:
+            return 5
+        }
+    }
+
+    private var circleSize: CGFloat {
+        switch deviceType {
+        case .watch40mm:
+            return 72
+        case .watch41mm,
+             .watch42mm:
+            return 74
+        case .watch44mm:
+            return 82
+        case .unknown,
+             .watch45mm:
+            return 90
+        case .watch49mm:
+            return 92
+        }
+    }
+
+    private var triangleSize: CGFloat {
+        switch deviceType {
+        case .watch40mm,
+             .watch41mm,
+             .watch42mm:
+            return 16
+        case .watch44mm:
+            return 18
+        case .unknown,
+             .watch45mm:
+            return 20
+        case .watch49mm:
+            return 20
+        }
+    }
+
+    private var triangleOffset: CGFloat {
+        switch deviceType {
+        case .watch40mm:
+            return 46
+        case .watch41mm,
+             .watch42mm:
+            return 47.5
+        case .watch44mm:
+            return 53.5
+        case .unknown,
+             .watch45mm:
+            return 58
+        case .watch49mm:
+            return 59
+        }
+    }
+
+    var body: some View {
+        ZStack {
+            // Outer circle with gradient
+            Circle()
+                .stroke(isWatchStateDated ? staleWatchStateGradient : angularGradient, lineWidth: strokeWidth)
+                .frame(width: circleSize, height: circleSize)
+                .background(Circle().fill(Color.black))
+
+            // Triangle with the color of the last gradient color
+            Triangle(deviceType: deviceType)
+                .fill(triangleColor)
+                .frame(width: triangleSize, height: triangleSize)
+                .offset(x: triangleOffset)
+                .opacity(isWatchStateDated ? 0 : 1)
+        }
+        .rotationEffect(.degrees(rotationDegrees))
+        .shadow(color: Color.black.opacity(0.33), radius: 3)
+    }
+}

+ 271 - 0
Trio Watch App Extension/Views/TrioMainWatchView.swift

@@ -0,0 +1,271 @@
+import Charts
+import SwiftUI
+import WatchKit
+
+struct TrioMainWatchView: View {
+    @State private var state = WatchState()
+
+    // misc
+    @State private var currentPage: Int = 0
+    @State private var rotationDegrees: Double = 0.0
+    @State private var showingTempTargetSheet = false
+
+    // view visbility
+    @State private var showingTreatmentMenuSheet: Bool = false
+    @State private var showingOverrideSheet: Bool = false
+    // navigation flag for meal bolus combo
+    @State private var continueToBolus = false
+    @State private var navigationPath = NavigationPath()
+
+    // treatments
+    @State private var selectedTreatment: TreatmentOption?
+
+    var isWatchStateDated: Bool {
+        // If `lastWatchStateUpdate` is nil, treat as "dated"
+        guard let lastUpdateTimestamp = state.lastWatchStateUpdate else {
+            return true
+        }
+        let now = Date().timeIntervalSince1970
+        let secondsSinceUpdate = now - lastUpdateTimestamp
+        // Return true if last update older than 5 min, so 1 loop cycle
+        return secondsSinceUpdate > 5 * 60
+    }
+
+    var isSessionUnreachable: Bool {
+        guard let session = state.session else {
+            return true // No session at all => unreachable
+        }
+        // Return true if not .activated OR not reachable
+        return session.activationState != .activated
+    }
+
+    // Active adjustment indicator
+    private func isAdjustmentActive<T>(for presets: [T], predicate: (T) -> Bool) -> Bool {
+        let sortedPresets = presets.sorted { predicate($0) && !predicate($1) }
+        return !sortedPresets.isEmpty && sortedPresets.first(where: predicate) != nil
+    }
+
+    private var isTempTargetActive: Bool {
+        isAdjustmentActive(for: state.tempTargetPresets) { $0.isEnabled }
+    }
+
+    private var isOverrideActive: Bool {
+        isAdjustmentActive(for: state.overridePresets) { $0.isEnabled }
+    }
+
+    private var trioBackgroundColor = LinearGradient(
+        gradient: Gradient(colors: [Color.bgDarkBlue, Color.bgDarkerDarkBlue]),
+        startPoint: .top,
+        endPoint: .bottom
+    )
+
+    var body: some View {
+        NavigationStack(path: $navigationPath) {
+            TabView(selection: $currentPage) {
+                // Page 1: Current glucose trend in "BG bobble"
+                ZStack {
+                    GlucoseTrendView(
+                        state: state,
+                        rotationDegrees: rotationDegrees,
+                        isWatchStateDated: isWatchStateDated || isSessionUnreachable
+                    )
+
+                    if state.showSyncingAnimation {
+                        Image(systemName: "iphone.radiowaves.left.and.right")
+                            .symbolRenderingMode(.palette)
+                            .foregroundStyle(Color.primary, Color.tabBar, Color.clear)
+                            .symbolEffect(
+                                .variableColor.iterative,
+                                options: .repeating,
+                                value: state.showSyncingAnimation
+                            )
+                            .position(
+                                x: 20,
+                                y: (WKInterfaceDevice.current().screenBounds.height / 4) -
+                                    7 // Font .body == 14, so half of default size for the SF Symbol image
+                            )
+                    }
+                }.tag(0)
+
+                // Page 2: Glucose chart
+                GlucoseChartView(glucoseValues: state.glucoseValues)
+                    .tag(1)
+            }
+            .onAppear {
+                // Hard reset variables when main view appears
+                /// Reset `bolusProgress` and `activeBolusAmount` to ensure no stale bolus progressbar is stuck on home view
+                state.bolusProgress = 0
+                state.activeBolusAmount = 0
+                /// Reset `bolusAmount` and `recommendedBolus` to ensure no stale / old value is set when user opens bolus input or meal combo the next time.
+                state.bolusAmount = 0
+                state.recommendedBolus = 0
+            }
+            .background(trioBackgroundColor)
+            .tabViewStyle(.verticalPage)
+            .digitalCrownRotation($currentPage.doubleBinding(), from: 0, through: 1, by: 1)
+            .onChange(of: state.trend) { _, newTrend in
+                withAnimation {
+                    updateRotation(for: newTrend)
+                }
+            }
+            .toolbar {
+                ToolbarItem(placement: .topBarLeading) {
+                    HStack {
+                        Image(systemName: "syringe.fill")
+                            .foregroundStyle(Color.insulin)
+
+                        Text(isWatchStateDated || isSessionUnreachable ? "--" : state.iob ?? "--")
+                            .foregroundStyle(isWatchStateDated ? Color.secondary : Color.white)
+                    }.font(.caption2)
+                }
+
+                ToolbarItem(placement: .topBarTrailing) {
+                    HStack {
+                        Text(isWatchStateDated || isSessionUnreachable ? "--" : state.cob ?? "--")
+                            .foregroundStyle(isWatchStateDated || isSessionUnreachable ? Color.secondary : Color.white)
+
+                        Image(systemName: "fork.knife")
+                            .foregroundStyle(Color.orange)
+                    }.font(.caption2)
+                }
+
+                ToolbarItemGroup(placement: .bottomBar) {
+                    Button {
+                        showingOverrideSheet = true
+                    } label: {
+                        Image(systemName: "clock.arrow.2.circlepath")
+                            .foregroundStyle(Color.primary, isOverrideActive ? Color.primary : Color.purple)
+                    }.tint(isOverrideActive ? Color.purple : nil)
+
+                    Button {
+                        showingTreatmentMenuSheet = true
+                    } label: {
+                        Image(systemName: "plus")
+                            .foregroundStyle(Color.bgDarkerDarkBlue)
+                    }
+                    .controlSize(.large)
+                    .buttonStyle(WatchOSButtonStyle(deviceType: state.deviceType))
+
+                    Button {
+                        showingTempTargetSheet = true
+                    } label: {
+                        Image(systemName: "target")
+                            .foregroundStyle(isTempTargetActive ? Color.primary : Color.loopGreen.opacity(0.75))
+                    }.tint(isTempTargetActive ? Color.loopGreen.opacity(0.75) : nil)
+                }
+            }
+            .fullScreenCover(isPresented: $showingTreatmentMenuSheet) {
+                TreatmentMenuView(deviceType: state.deviceType, selectedTreatment: $selectedTreatment) {
+                    handleTreatmentSelection()
+                }
+                .onAppear {
+                    // reset the conditional navigation flag when opening
+                    continueToBolus = false
+                }
+            }
+            .sheet(isPresented: $showingOverrideSheet) {
+                OverridePresetsView(
+                    state: state,
+                    overridePresets: state.overridePresets
+                ) {
+                    showingOverrideSheet = false
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }
+            }
+            .sheet(isPresented: $showingTempTargetSheet) {
+                TempTargetPresetsView(
+                    state: state,
+                    tempTargetPresets: state.tempTargetPresets
+                ) {
+                    showingTempTargetSheet = false
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }
+            }
+            .navigationDestination(for: NavigationDestinations.self) { destination in
+                switch destination {
+                case .acknowledgmentPending:
+                    AcknowledgementPendingView(
+                        navigationPath: $navigationPath,
+                        state: state,
+                        shouldNavigateToRoot: $state.shouldNavigateToRoot
+                    )
+                case .carbsInput:
+                    CarbsInputView(
+                        navigationPath: $navigationPath,
+                        state: state,
+                        continueToBolus: continueToBolus
+                    )
+                case .bolusInput:
+                    BolusInputView(
+                        navigationPath: $navigationPath,
+                        state: state
+                    )
+                case .bolusConfirm:
+                    BolusConfirmationView(
+                        navigationPath: $navigationPath,
+                        state: state,
+                        bolusAmount: $state.bolusAmount,
+                        confirmationProgress: $state.confirmationProgress
+                    )
+                }
+            }
+            .onChange(of: navigationPath) { _, newPath in
+                if newPath.isEmpty {
+                    // Reset conditional view navigation when returning to root view
+                    continueToBolus = false
+                }
+            }
+        }
+        .ignoresSafeArea()
+        .blur(radius: state.showBolusProgressOverlay ? 3 : 0)
+        .overlay {
+            if state.showBolusProgressOverlay {
+                BolusProgressOverlay(state: state) {
+                    state.shouldNavigateToRoot = false
+                    navigationPath.append(NavigationDestinations.acknowledgmentPending)
+                }.transition(.opacity)
+            }
+        }
+    }
+
+    private func updateRotation(for trend: String?) {
+        switch trend {
+        case "DoubleUp",
+             "SingleUp":
+            rotationDegrees = -90
+        case "FortyFiveUp":
+            rotationDegrees = -45
+        case "Flat":
+            rotationDegrees = 0
+        case "FortyFiveDown":
+            rotationDegrees = 45
+        case "DoubleDown",
+             "SingleDown":
+            rotationDegrees = 90
+        default:
+            rotationDegrees = 0
+        }
+    }
+
+    private func handleTreatmentSelection() {
+        showingTreatmentMenuSheet = false // Dismiss the sheet
+
+        guard let treatment = selectedTreatment else { return }
+
+        switch treatment {
+        case .meal:
+            navigationPath.append(NavigationDestinations.carbsInput)
+        case .bolus:
+            // Reset carbs amount when directly going to bolus input
+            state.carbsAmount = 0
+            navigationPath.append(NavigationDestinations.bolusInput)
+        case .mealBolusCombo:
+            continueToBolus = true // Explicitely set subsequent view navigation
+            navigationPath.append(NavigationDestinations.carbsInput)
+        }
+    }
+}
+
+#Preview {
+    TrioMainWatchView()
+}

+ 170 - 0
Trio Watch App Extension/WatchState+Requests.swift

@@ -0,0 +1,170 @@
+import Foundation
+import WatchConnectivity
+
+// MARK: - Send Data to Phone
+
+extension WatchState {
+    /// Sends a bolus insulin request to the paired iPhone
+    /// - Parameters:
+    ///   - amount: The insulin amount to be delivered
+    func sendBolusRequest(_ amount: Decimal) {
+        guard let session = session, session.isReachable else { return }
+        isBolusCanceled = false // Reset canceled state when starting new bolus
+        activeBolusAmount = Double(truncating: amount as NSNumber) // Set active bolus amount
+
+        let message: [String: Any] = [
+            WatchMessageKeys.bolus: amount
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("Error sending bolus request: \(error.localizedDescription)")
+        }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a carbohydrate entry request to the paired iPhone
+    /// - Parameters:
+    ///   - amount: The amount of carbs in grams
+    ///   - date: The timestamp for the carb entry (defaults to current time)
+    func sendCarbsRequest(_ amount: Int, _ date: Date = Date()) {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.carbs: amount,
+            WatchMessageKeys.date: date.timeIntervalSince1970
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("Error sending carbs request: \(error.localizedDescription)")
+        }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a request to cancel the current override preset to the paired iPhone
+    func sendCancelOverrideRequest() {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.cancelOverride: true
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("⌚️ Error sending cancel override request: \(error.localizedDescription)")
+        }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a request to activate an override preset to the paired iPhone
+    /// - Parameter presetName: The name of the override preset to activate
+    func sendActivateOverrideRequest(presetName: String) {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.activateOverride: presetName
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("⌚️ Error sending activate override request: \(error.localizedDescription)")
+        }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a request to cancel the current temporary target to the paired iPhone
+    func sendCancelTempTargetRequest() {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.cancelTempTarget: true
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("⌚️ Error sending cancel temp target request: \(error.localizedDescription)")
+        }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a request to activate a temporary target preset to the paired iPhone
+    /// - Parameter presetName: The name of the temporary target preset to activate
+    func sendActivateTempTargetRequest(presetName: String) {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.activateTempTarget: presetName
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("⌚️ Error sending activate temp target request: \(error.localizedDescription)")
+        }
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a request to cancel the current bolus delivery to the paired iPhone
+    func sendCancelBolusRequest() {
+        isBolusCanceled = true
+
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.cancelBolus: true
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("Error sending cancel bolus request: \(error.localizedDescription)")
+        }
+
+        // Reset when cancelled
+        bolusProgress = 0
+        activeBolusAmount = 0
+
+        // Display pending communication animation
+        showCommsAnimation = true
+    }
+
+    /// Sends a request to calculate a bolus recommendation based on the current carbs amount
+    func requestBolusRecommendation() {
+        guard let session = session, session.isReachable else { return }
+
+        let message: [String: Any] = [
+            WatchMessageKeys.requestBolusRecommendation: true,
+            WatchMessageKeys.carbs: carbsAmount
+        ]
+
+        session.sendMessage(message, replyHandler: nil) { error in
+            print("Error requesting bolus recommendation: \(error.localizedDescription)")
+        }
+
+        showBolusCalculationProgress = true
+    }
+
+    func requestWatchStateUpdate() {
+        guard let session = session, session.activationState == .activated else {
+            print("⌚️ Session not activated, activating...")
+            session?.activate()
+            return
+        }
+
+        if session.isReachable {
+            print("⌚️ Request an update for watch state from Trio iPhone app...")
+
+            let message = [WatchMessageKeys.requestWatchUpdate: WatchMessageKeys.watchState]
+
+            session.sendMessage(message, replyHandler: nil) { error in
+                print("⌚️ Update request for fresh watch state data: \(error.localizedDescription)")
+            }
+        } else {
+            print("⌚️ Phone not reachable for watch state update")
+        }
+    }
+}

+ 588 - 0
Trio Watch App Extension/WatchState.swift

@@ -0,0 +1,588 @@
+import Foundation
+import SwiftUI
+import WatchConnectivity
+
+/// WatchState manages the communication between the Watch app and the iPhone app using WatchConnectivity.
+/// It handles glucose data synchronization and sending treatment requests (bolus, carbs) to the phone.
+@Observable final class WatchState: NSObject, WCSessionDelegate {
+    // MARK: - Properties
+
+    /// The WatchConnectivity session instance used for communication
+    var session: WCSession?
+    /// Indicates if the paired iPhone is currently reachable
+    var isReachable = false
+
+    var lastWatchStateUpdate: TimeInterval?
+
+    /// main view relevant metrics
+    var currentGlucose: String = "--"
+    var currentGlucoseColorString: String = "#ffffff"
+    var trend: String? = ""
+    var delta: String? = "--"
+    var glucoseValues: [(date: Date, glucose: Double, color: Color)] = []
+    var cob: String? = "--"
+    var iob: String? = "--"
+    var lastLoopTime: String? = "--"
+    var overridePresets: [OverridePresetWatch] = []
+    var tempTargetPresets: [TempTargetPresetWatch] = []
+
+    /// treatments inputs
+    /// used to store carbs for combined meal-bolus-treatments
+    var carbsAmount: Int = 0
+    var fatAmount: Int = 0
+    var proteinAmount: Int = 0
+    var bolusAmount: Double = 0.0
+    var confirmationProgress: Double = 0.0
+
+    var bolusProgress: Double = 0.0
+    var activeBolusAmount: Double = 0.0
+    var deliveredAmount: Double = 0.0
+    var isBolusCanceled = false
+
+    // Safety limits
+    var maxBolus: Decimal = 10
+    var maxCarbs: Decimal = 250
+    var maxFat: Decimal = 250
+    var maxProtein: Decimal = 250
+
+    // Pump specific dosing increment
+    var bolusIncrement: Decimal = 0.05
+    var confirmBolusFaster: Bool = false
+
+    // Acknowlegement handling
+    var showCommsAnimation: Bool = false
+    var showAcknowledgmentBanner: Bool = false
+    var acknowledgementStatus: AcknowledgementStatus = .pending
+    var acknowledgmentMessage: String = ""
+    var shouldNavigateToRoot: Bool = true
+
+    // Bolus calculation progress
+    var showBolusCalculationProgress: Bool = false
+
+    // Meal bolus-specific properties
+    var mealBolusStep: MealBolusStep = .savingCarbs
+    var isMealBolusCombo: Bool = false
+
+    var showBolusProgressOverlay: Bool {
+        (!showAcknowledgmentBanner || !showCommsAnimation) && bolusProgress > 0 && bolusProgress < 1.0 && !isBolusCanceled
+    }
+
+    var recommendedBolus: Decimal = 0
+
+    // Debouncing and batch processing helpers
+
+    /// Temporary storage for new data arriving via WatchConnectivity.
+    private var pendingData: [String: Any] = [:]
+
+    /// Work item to schedule finalizing the pending data.
+    private var finalizeWorkItem: DispatchWorkItem?
+
+    /// A flag to tell the UI we’re still updating.
+    var showSyncingAnimation: Bool = false
+
+    var deviceType = WatchSize.current
+
+    override init() {
+        super.init()
+        setupSession()
+    }
+
+    /// Configures the WatchConnectivity session if supported on the device
+    private func setupSession() {
+        if WCSession.isSupported() {
+            let session = WCSession.default
+            session.delegate = self
+            session.activate()
+            self.session = session
+        } else {
+            print("⌚️ WCSession is not supported on this device")
+        }
+    }
+
+    // MARK: – Handle Acknowledgement Messages FROM Phone
+
+    func handleAcknowledgment(success: Bool, message: String, isFinal: Bool = true) {
+        if success {
+            print("⌚️ Acknowledgment received: \(message)")
+            acknowledgementStatus = .success
+            acknowledgmentMessage = "\(message)"
+        } else {
+            print("⌚️ Acknowledgment failed: \(message)")
+            acknowledgementStatus = .failure
+            acknowledgmentMessage = "\(message)"
+        }
+
+        DispatchQueue.main.async {
+            self.showCommsAnimation = false // Hide progress animation
+            self.showSyncingAnimation = false // Just ensure this is 100% set to false
+        }
+
+        if isFinal {
+            showAcknowledgmentBanner = true
+            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                self.showAcknowledgmentBanner = false
+                self.showSyncingAnimation = false // Just ensure this is 100% set to false
+            }
+        }
+    }
+
+    // MARK: - WCSessionDelegate
+
+    /// Called when the session has completed activation
+    /// Updates the reachability status and logs the activation state
+    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
+        DispatchQueue.main.async {
+            if let error = error {
+                print("⌚️ Watch session activation failed: \(error.localizedDescription)")
+                return
+            }
+
+            if activationState == .activated {
+                print("⌚️ Watch session activated with state: \(activationState.rawValue)")
+
+                self.forceConditionalWatchStateUpdate()
+
+                self.isReachable = session.isReachable
+
+                print("⌚️ Watch isReachable after activation: \(session.isReachable)")
+            }
+        }
+    }
+
+    /// Handles incoming messages from the paired iPhone when Phone is in the foreground
+    func session(_: WCSession, didReceiveMessage message: [String: Any]) {
+        print("⌚️ Watch received data: \(message)")
+
+        // If the message has a nested "watchState" dictionary with date as TimeInterval
+        if let watchStateDict = message[WatchMessageKeys.watchState] as? [String: Any],
+           let timestamp = watchStateDict[WatchMessageKeys.date] as? TimeInterval
+        {
+            let date = Date(timeIntervalSince1970: timestamp)
+
+            // Check if it's not older than 15 min
+            if date >= Date().addingTimeInterval(-15 * 60) {
+                print("⌚️ Handling watchState from \(date)")
+                processWatchMessage(message)
+            } else {
+                print("⌚️ Received outdated watchState data (\(date))")
+                DispatchQueue.main.async {
+                    self.showSyncingAnimation = false
+                }
+            }
+            return
+        }
+
+        // Else if the message is an "ack" at the top level
+        // e.g. { "acknowledged": true, "message": "Started Temp Target...", "date": Date(...) }
+        else if
+            let acknowledged = message[WatchMessageKeys.acknowledged] as? Bool,
+            let ackMessage = message[WatchMessageKeys.message] as? String
+        {
+            print("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged)")
+            DispatchQueue.main.async {
+                // For ack messages, we do NOT show “Syncing...”
+                self.showSyncingAnimation = false
+            }
+            processWatchMessage(message)
+            return
+
+                    // Recommended bolus is also not part of the WatchState message, hence the extra condition here
+        } else if
+            let recommendedBolus = message[WatchMessageKeys.recommendedBolus] as? NSNumber
+        {
+            print("⌚️ Received recommended bolus: \(recommendedBolus)")
+
+            DispatchQueue.main.async {
+                self.recommendedBolus = recommendedBolus.decimalValue
+                self.showBolusCalculationProgress = false
+            }
+
+            return
+
+                    // Handle bolus progress updates
+        } else if
+            let timestamp = message[WatchMessageKeys.bolusProgressTimestamp] as? TimeInterval,
+            let progress = message[WatchMessageKeys.bolusProgress] as? Double,
+            let activeBolusAmount = message[WatchMessageKeys.activeBolusAmount] as? Double,
+            let deliveredAmount = message[WatchMessageKeys.deliveredAmount] as? Double
+        {
+            let date = Date(timeIntervalSince1970: timestamp)
+
+            // Check if it's not older than 5 min
+            if date >= Date().addingTimeInterval(-5 * 60) {
+                print("⌚️ Handling bolusProgress (sent at \(date))")
+                DispatchQueue.main.async {
+                    if !self.isBolusCanceled {
+                        self.bolusProgress = progress
+                        self.activeBolusAmount = activeBolusAmount
+                        self.deliveredAmount = deliveredAmount
+                    }
+                }
+            } else {
+                print("⌚️ Received outdated bolus progress (sent at \(date))")
+                DispatchQueue.main.async {
+                    self.bolusProgress = 0
+                    self.activeBolusAmount = 0
+                }
+            }
+            return
+
+                    // Handle bolus cancellation
+        } else if
+            message[WatchMessageKeys.bolusCanceled] as? Bool == true
+        {
+            DispatchQueue.main.async {
+                self.bolusProgress = 0
+                self.activeBolusAmount = 0
+                self
+                    .isBolusCanceled =
+                    false /// Reset flag to ensure a bolus progress is also shown after canceling bolus from watch
+            }
+            return
+        } else {
+            print("⌚️ Faulty data. Skipping...")
+            DispatchQueue.main.async {
+                self.showSyncingAnimation = false
+            }
+        }
+    }
+
+    /// Handles incoming messages from the paired iPhone when Phone is in the background
+    func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
+        print("⌚️ Watch received data: \(userInfo)")
+
+        // If the message has a nested "watchState" dictionary with date as TimeInterval
+        if let watchStateDict = userInfo[WatchMessageKeys.watchState] as? [String: Any],
+           let timestamp = watchStateDict[WatchMessageKeys.date] as? TimeInterval
+        {
+            let date = Date(timeIntervalSince1970: timestamp)
+
+            // Check if it's not older than 15 min
+            if date >= Date().addingTimeInterval(-15 * 60) {
+                print("⌚️ Handling watchState from \(date)")
+                processWatchMessage(userInfo)
+            } else {
+                print("⌚️ Received outdated watchState data (\(date))")
+                DispatchQueue.main.async {
+                    self.showSyncingAnimation = false
+                }
+            }
+            return
+        }
+
+        // Else if the message is an "ack" at the top level
+        // e.g. { "acknowledged": true, "message": "Started Temp Target...", "date": Date(...) }
+        else if
+            let acknowledged = userInfo[WatchMessageKeys.acknowledged] as? Bool,
+            let ackMessage = userInfo[WatchMessageKeys.message] as? String
+        {
+            print("⌚️ Handling ack with message: \(ackMessage), success: \(acknowledged)")
+            DispatchQueue.main.async {
+                // For ack messages, we do NOT show “Syncing...”
+                self.showSyncingAnimation = false
+            }
+            processWatchMessage(userInfo)
+            return
+
+                    // Recommended bolus is also not part of the WatchState message, hence the extra condition here
+        } else if
+            let recommendedBolus = userInfo[WatchMessageKeys.recommendedBolus] as? NSNumber
+        {
+            print("⌚️ Received recommended bolus: \(recommendedBolus)")
+            self.recommendedBolus = recommendedBolus.decimalValue
+            showBolusCalculationProgress = false
+            return
+
+                    // Handle bolus progress updates
+        } else if
+            let timestamp = userInfo[WatchMessageKeys.bolusProgressTimestamp] as? TimeInterval,
+            let progress = userInfo[WatchMessageKeys.bolusProgress] as? Double,
+            let activeBolusAmount = userInfo[WatchMessageKeys.activeBolusAmount] as? Double,
+            let deliveredAmount = userInfo[WatchMessageKeys.deliveredAmount] as? Double
+        {
+            let date = Date(timeIntervalSince1970: timestamp)
+
+            // Check if it's not older than 5 min
+            if date >= Date().addingTimeInterval(-5 * 60) {
+                print("⌚️ Handling bolusProgress (sent at \(date))")
+                DispatchQueue.main.async {
+                    if !self.isBolusCanceled {
+                        self.bolusProgress = progress
+                        self.activeBolusAmount = activeBolusAmount
+                        self.deliveredAmount = deliveredAmount
+                    }
+                }
+            } else {
+                print("⌚️ Received outdated bolus progress (sent at \(date))")
+                DispatchQueue.main.async {
+                    self.bolusProgress = 0
+                    self.activeBolusAmount = 0
+                }
+            }
+            return
+
+                    // Handle bolus cancellation
+        } else if
+            userInfo[WatchMessageKeys.bolusCanceled] as? Bool == true
+        {
+            DispatchQueue.main.async {
+                self.bolusProgress = 0
+                self.activeBolusAmount = 0
+            }
+            return
+        } else {
+            print("⌚️ Faulty data. Skipping...")
+            DispatchQueue.main.async {
+                self.showSyncingAnimation = false
+            }
+        }
+    }
+
+    /// Called when the reachability status of the paired iPhone changes
+    /// Updates the local reachability status
+    func sessionReachabilityDidChange(_ session: WCSession) {
+        DispatchQueue.main.async {
+            print("⌚️ Watch reachability changed: \(session.isReachable)")
+
+            if session.isReachable {
+                self.forceConditionalWatchStateUpdate()
+
+                // reset input amounts
+                self.bolusAmount = 0
+                self.carbsAmount = 0
+                // reset auth progress
+                self.confirmationProgress = 0
+            }
+        }
+    }
+
+    /// Conditionally triggers a watch state update if the last known update was too long ago or has never occurred.
+    ///
+    /// This method checks the `lastWatchStateUpdate` timestamp to determine how many seconds
+    /// have elapsed since the last update under the following conditions
+    ///  - If `lastWatchStateUpdate` is `nil` (meaning there has never been an update), or
+    ///  - If more than 15 seconds have passed,
+    ///
+    /// it will show a syncing animation and request a new watch state update from the iPhone app.
+    private func forceConditionalWatchStateUpdate() {
+        guard let lastUpdateTimestamp = lastWatchStateUpdate else {
+            // If there's no recorded timestamp, we must force a fresh update immediately.
+            showSyncingAnimation = true
+            requestWatchStateUpdate()
+            return
+        }
+
+        let now = Date().timeIntervalSince1970
+        let secondsSinceUpdate = now - lastUpdateTimestamp
+
+        // If more than 15 seconds have elapsed since the last update, force an(other) update.
+        if secondsSinceUpdate > 15 {
+            showSyncingAnimation = true
+            requestWatchStateUpdate()
+            return
+        }
+    }
+
+    /// Handles incoming messages that either contain an acknowledgement or fresh watchState data  (<15 min)
+    private func processWatchMessage(_ message: [String: Any]) {
+        DispatchQueue.main.async {
+            // 1) Acknowledgment logic
+            if let acknowledged = message[WatchMessageKeys.acknowledged] as? Bool,
+               let ackMessage = message[WatchMessageKeys.message] as? String
+            {
+                DispatchQueue.main.async {
+                    self.showSyncingAnimation = false
+                }
+
+                print("⌚️ Received acknowledgment: \(ackMessage), success: \(acknowledged)")
+
+                switch ackMessage {
+                case "Saving carbs...":
+                    self.isMealBolusCombo = true
+                    self.mealBolusStep = .savingCarbs
+                    self.showCommsAnimation = true
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
+                case "Enacting bolus...":
+                    self.isMealBolusCombo = true
+                    self.mealBolusStep = .enactingBolus
+                    self.showCommsAnimation = true
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: false)
+                case "Carbs and bolus logged successfully":
+                    self.isMealBolusCombo = false
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
+                default:
+                    self.isMealBolusCombo = false
+                    self.handleAcknowledgment(success: acknowledged, message: ackMessage, isFinal: true)
+                }
+            }
+
+            // 2) Raw watchState data
+            if let watchStateData = message[WatchMessageKeys.watchState] as? [String: Any] {
+                self.scheduleUIUpdate(with: watchStateData)
+            }
+        }
+    }
+
+    /// Accumulate new data, set isSyncing, and debounce final update
+    private func scheduleUIUpdate(with newData: [String: Any]) {
+        // 1) Mark as syncing
+        DispatchQueue.main.async {
+            self.showSyncingAnimation = true
+        }
+
+        // 2) Merge data into our pendingData
+        pendingData.merge(newData) { _, newVal in newVal }
+
+        // 3) Cancel any previous finalization
+        finalizeWorkItem?.cancel()
+
+        // 4) Create and schedule a new finalization
+        let workItem = DispatchWorkItem { [self] in
+            self.finalizePendingData()
+        }
+        finalizeWorkItem = workItem
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: workItem)
+    }
+
+    /// Applies all pending data to the watch state in one shot
+    private func finalizePendingData() {
+        guard !pendingData.isEmpty else {
+            // If we have no actual data, just end syncing
+            DispatchQueue.main.async {
+                self.showSyncingAnimation = false
+            }
+            return
+        }
+
+        print("⌚️ Finalizing pending data: \(pendingData)")
+
+        // Actually set your main UI properties here
+        processRawDataForWatchState(pendingData)
+
+        // Clear
+        pendingData.removeAll()
+
+        // Done - hide sync animation
+        DispatchQueue.main.async {
+            self.showSyncingAnimation = false
+        }
+    }
+
+    /// Updates the UI properties
+    private func processRawDataForWatchState(_ message: [String: Any]) {
+        if let timestamp = message[WatchMessageKeys.date] as? TimeInterval {
+            lastWatchStateUpdate = timestamp
+        }
+
+        if let currentGlucose = message[WatchMessageKeys.currentGlucose] as? String {
+            self.currentGlucose = currentGlucose
+        }
+
+        if let currentGlucoseColorString = message[WatchMessageKeys.currentGlucoseColorString] as? String {
+            self.currentGlucoseColorString = currentGlucoseColorString
+        }
+
+        if let trend = message[WatchMessageKeys.trend] as? String {
+            self.trend = trend
+        }
+
+        if let delta = message[WatchMessageKeys.delta] as? String {
+            self.delta = delta
+        }
+
+        if let iob = message[WatchMessageKeys.iob] as? String {
+            self.iob = iob
+        }
+
+        if let cob = message[WatchMessageKeys.cob] as? String {
+            self.cob = cob
+        }
+
+        if let lastLoopTime = message[WatchMessageKeys.lastLoopTime] as? String {
+            self.lastLoopTime = lastLoopTime
+        }
+
+        if let glucoseData = message[WatchMessageKeys.glucoseValues] as? [[String: Any]] {
+            glucoseValues = glucoseData.compactMap { data in
+                guard let glucose = data["glucose"] as? Double,
+                      let timestamp = data["date"] as? TimeInterval,
+                      let colorString = data["color"] as? String
+                else { return nil }
+
+                return (
+                    Date(timeIntervalSince1970: timestamp),
+                    glucose,
+                    colorString.toColor() // Convert colorString to Color
+                )
+            }
+            .sorted { $0.date < $1.date }
+        }
+
+        if let overrideData = message[WatchMessageKeys.overridePresets] as? [[String: Any]] {
+            overridePresets = overrideData.compactMap { data in
+                guard let name = data["name"] as? String,
+                      let isEnabled = data["isEnabled"] as? Bool
+                else { return nil }
+                return OverridePresetWatch(name: name, isEnabled: isEnabled)
+            }
+        }
+
+        if let tempTargetData = message[WatchMessageKeys.tempTargetPresets] as? [[String: Any]] {
+            tempTargetPresets = tempTargetData.compactMap { data in
+                guard let name = data["name"] as? String,
+                      let isEnabled = data["isEnabled"] as? Bool
+                else { return nil }
+                return TempTargetPresetWatch(name: name, isEnabled: isEnabled)
+            }
+        }
+
+        if let bolusProgress = message[WatchMessageKeys.bolusProgress] as? Double {
+            if !isBolusCanceled {
+                self.bolusProgress = bolusProgress
+            }
+        }
+
+        if let bolusWasCanceled = message[WatchMessageKeys.bolusCanceled] as? Bool, bolusWasCanceled {
+            bolusProgress = 0
+            activeBolusAmount = 0
+        }
+
+        if let maxBolusValue = message[WatchMessageKeys.maxBolus] {
+            print("⌚️ Received maxBolus: \(maxBolusValue) of type \(type(of: maxBolusValue))")
+            if let decimalValue = (maxBolusValue as? NSNumber)?.decimalValue {
+                maxBolus = decimalValue
+                print("⌚️ Converted maxBolus to: \(decimalValue)")
+            }
+        }
+
+        if let maxCarbsValue = message[WatchMessageKeys.maxCarbs] {
+            if let decimalValue = (maxCarbsValue as? NSNumber)?.decimalValue {
+                maxCarbs = decimalValue
+            }
+        }
+
+        if let maxFatValue = message[WatchMessageKeys.maxFat] {
+            if let decimalValue = (maxFatValue as? NSNumber)?.decimalValue {
+                maxFat = decimalValue
+            }
+        }
+
+        if let maxProteinValue = message[WatchMessageKeys.maxProtein] {
+            if let decimalValue = (maxProteinValue as? NSNumber)?.decimalValue {
+                maxProtein = decimalValue
+            }
+        }
+
+        if let bolusIncrement = message[WatchMessageKeys.bolusIncrement] {
+            if let decimalValue = (bolusIncrement as? NSNumber)?.decimalValue {
+                self.bolusIncrement = decimalValue
+            }
+        }
+
+        if let confirmBolusFaster = message[WatchMessageKeys.confirmBolusFaster] {
+            if let booleanValue = confirmBolusFaster as? Bool {
+                self.confirmBolusFaster = booleanValue
+            }
+        }
+    }
+}

+ 0 - 0
Trio Watch App Tests/Unit Tests.swift


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است