import SwiftUI import SFSafeSymbols struct TransferView: View { private let storageWarnBytes = 500 let bluetoothClient: BluetoothDevice @Binding var info: DeviceInfo? @EnvironmentObject var storage: PersistentStorage @EnvironmentObject var transfer: TransferHandler private var storageIcon: SFSymbol { guard let info else { return .externaldrive } if info.storageSize - info.numberOfRecordedBytes < storageWarnBytes { return .externaldriveTrianglebadgeExclamationmark } return .externaldrive } private var measurementsText: String { guard let info else { return "No measurements" } return "\(info.numberOfStoredMeasurements) measurements (\(info.time.totalNumberOfMeasurements) total)" } private var storageText: String { guard let info else { return "No data" } if info.storageSize <= 0 { return "\(info.numberOfRecordedBytes) Bytes" } return "\(info.numberOfRecordedBytes) / \(info.storageSize) Bytes (\(info.storageFillPercentage) %)" } private var transferSizeText: String { guard let info else { return "No transfer size" } return "\(info.transferBlockSize) Byte Block Size" } private var transferByteText: String { let total = Int(transfer.totalBytes) guard total > 0 else { return "No data" } return "\(Int(transfer.bytesTransferred)) / \(total) Bytes" } private var transferMeasurementText: String { guard !transfer.measurements.isEmpty else { return "No measurements" } return "\(transfer.measurements.count) measurements" } var body: some View { NavigationView { VStack { VStack(alignment: .leading, spacing: 5) { Text("Storage") .font(.headline) IconAndTextView( icon: .speedometer, text: measurementsText) IconAndTextView( icon: storageIcon, text: storageText) IconAndTextView( icon: .iphoneAndArrowForward, text: transferSizeText) } Button(action: clearStorage) { Text("Remove recorded data") } .disabled(transfer.transferIsRunning) .padding() VStack(alignment: .leading, spacing: 5) { Text("Transfer") .font(.headline) ProgressView(value: transfer.bytesTransferred, total: transfer.totalBytes) .progressViewStyle(.linear) .padding(.vertical, 5) IconAndTextView( icon: .externaldrive, text: transferByteText) IconAndTextView( icon: .speedometer, text: transferMeasurementText) } HStack { Button(action: transferData) { Text("Transfer") } .disabled(transfer.transferIsRunning) .padding() Spacer() Button(action: saveTransfer) { Text("Save") } .disabled(transfer.transferIsRunning || transfer.measurements.isEmpty) .padding() Spacer() Button(action: discardTransfer) { Text("Discard") } .disabled(transfer.transferIsRunning || transfer.measurements.isEmpty) .padding() } Spacer() VStack { } } .padding() .navigationTitle("Data Transfer") .navigationBarTitleDisplayMode(.large) } } func transferData() { guard let info else { return } transfer.startTransfer(from: bluetoothClient, with: info, storage: storage) } func discardTransfer() { transfer.discardTransfer() } func saveTransfer() { transfer.saveTransfer(in: storage) } func clearStorage() { guard let byteCount = info?.numberOfRecordedBytes else { return } Task { guard await bluetoothClient.deleteDeviceData(byteCount: byteCount) else { log.warning("Failed to delete data") return } log.warning("Device storage cleared") } } } struct TransferView_Previews: PreviewProvider { static var previews: some View { let storage = PersistentStorage(lastMeasurements: TemperatureMeasurement.mockData) TransferView(bluetoothClient: .init(), info: .constant(.mock)) .environmentObject(storage) } } private extension TemperatureValue { var relativeValue: Double { if case .value(let double) = self { return double } return 0 } }