Sfoglia il codice sorgente

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

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

+ 1 - 1
DanaKit

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

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


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

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

+ 0 - 106
FreeAPSWatch WatchKit Extension/ComplicationController.swift

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

+ 0 - 33
FreeAPSWatch WatchKit Extension/DataFlow.swift

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

+ 0 - 15
FreeAPSWatch WatchKit Extension/FreeAPSApp.swift

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

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

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

+ 0 - 22
FreeAPSWatch WatchKit Extension/Info.plist

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

+ 0 - 25
FreeAPSWatch WatchKit Extension/NotificationController.swift

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

+ 0 - 13
FreeAPSWatch WatchKit Extension/NotificationView.swift

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

+ 0 - 20
FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 0 - 201
FreeAPSWatch WatchKit Extension/WatchStateModel.swift

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

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

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

+ 1 - 0
LiveActivity/LiveActivity.swift

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

+ 1 - 1
LiveActivity/Views/LiveActivityBGAndTrendView.swift

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

+ 1 - 1
LiveActivity/Views/LiveActivityChartView.swift

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

+ 1 - 2
LiveActivity/Views/LiveActivityGlucoseDeltaLabelView.swift

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

+ 14 - 7
LiveActivity/Views/LiveActivityView.swift

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 19 - 9
Model/CoreDataObserver.swift

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

+ 1 - 0
Model/Helper/CustomNotification.swift

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

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

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

+ 1 - 1
Model/Helper/TempTargetRunStored.swift

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

+ 4 - 4
TempTargetRunStored+CoreDataProperties.swift

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

+ 6 - 6
TempTargetStored+CoreDataProperties.swift

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

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


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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Some files were not shown because too many files changed in this diff