// // ChargedDeviceEditorSheetView.swift // USB Meter // // Created by Codex on 10/04/2026. // import SwiftUI struct ChargedDeviceEditorSheetView: View { @EnvironmentObject private var appData: AppData @Environment(\.dismiss) private var dismiss let meterMACAddress: String? let chargedDevice: ChargedDeviceSummary? let suggestedDeviceClass: ChargedDeviceClass? @State private var name: String @State private var deviceClass: ChargedDeviceClass @State private var chargingStateAvailability: ChargingStateAvailability @State private var supportsWiredCharging: Bool @State private var supportsWirelessCharging: Bool @State private var preferredChargingTransportMode: ChargingTransportMode @State private var wirelessChargingProfile: WirelessChargingProfile @State private var completionCurrentTexts: [ChargeSessionKind: String] @State private var notes: String init( meterMACAddress: String?, chargedDevice: ChargedDeviceSummary? = nil, suggestedDeviceClass: ChargedDeviceClass? = nil ) { self.meterMACAddress = meterMACAddress self.chargedDevice = chargedDevice self.suggestedDeviceClass = suggestedDeviceClass let initialDeviceClass = chargedDevice?.deviceClass ?? suggestedDeviceClass ?? .iphone _name = State(initialValue: chargedDevice?.name ?? "") _deviceClass = State(initialValue: initialDeviceClass) _chargingStateAvailability = State(initialValue: chargedDevice?.chargingStateAvailability ?? Self.suggestedChargingStateAvailability(for: initialDeviceClass)) _supportsWiredCharging = State(initialValue: chargedDevice?.supportsWiredCharging ?? true) _supportsWirelessCharging = State(initialValue: chargedDevice?.supportsWirelessCharging ?? true) _preferredChargingTransportMode = State(initialValue: chargedDevice?.preferredChargingTransportMode ?? .wired) _wirelessChargingProfile = State(initialValue: chargedDevice?.wirelessChargingProfile ?? .genericQi) _completionCurrentTexts = State( initialValue: Self.makeCompletionCurrentTexts(for: chargedDevice) ) _notes = State(initialValue: chargedDevice?.notes ?? "") } var body: some View { NavigationView { Form { Section(header: Text("Identity")) { TextField("Name", text: $name) Picker("Class", selection: $deviceClass) { ForEach(ChargedDeviceClass.allCases) { deviceClass in Label(deviceClass.title, systemImage: deviceClass.symbolName) .tag(deviceClass) } } if let chargedDevice { Text(chargedDevice.qrIdentifier) .font(.caption.monospaced()) .foregroundColor(.secondary) .textSelection(.enabled) } } Section(header: Text("Charge Behaviour")) { Picker("Session Modes", selection: $chargingStateAvailability) { ForEach(ChargingStateAvailability.allCases) { availability in Text(availability.title) .tag(availability) } } Text(chargingStateAvailability.description) .font(.footnote) .foregroundColor(.secondary) } Section(header: Text("Charging Support")) { Toggle("Supports wired charging", isOn: $supportsWiredCharging) Toggle("Supports wireless charging", isOn: $supportsWirelessCharging) if supportsWirelessCharging { Picker("Wireless profile", selection: $wirelessChargingProfile) { ForEach(WirelessChargingProfile.allCases) { profile in Text(profile.title) .tag(profile) } } Text(wirelessChargingProfile.description) .font(.footnote) .foregroundColor(.secondary) } if !supportedChargingModes.isEmpty { Picker("Default session type", selection: preferredChargingTransportBinding) { ForEach(supportedChargingModes) { chargingTransportMode in Label(chargingTransportMode.title, systemImage: chargingTransportMode.symbolName) .tag(chargingTransportMode) } } } else { Text("Enable at least one charging method.") .font(.footnote) .foregroundColor(.secondary) } } Section(header: Text("Charge Completion")) { if applicableSessionKinds.isEmpty { Text("Enable at least one charging method to configure stop currents.") .font(.footnote) .foregroundColor(.secondary) } else { ForEach(applicableSessionKinds) { sessionKind in VStack(alignment: .leading, spacing: 6) { TextField( "\(sessionKind.shortTitle) completion current (A)", text: completionCurrentTextBinding(for: sessionKind) ) .keyboardType(.decimalPad) Text("Leave empty to keep learning this threshold from sessions of the same type.") .font(.caption) .foregroundColor(.secondary) } .padding(.vertical, 2) } } } Section(header: Text("Notes")) { TextField("Optional notes", text: $notes) } } .navigationTitle(editorTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(saveButtonTitle) { let configuredCompletionCurrents = parsedCompletionCurrents let didSave: Bool if let chargedDevice { didSave = appData.updateChargedDevice( id: chargedDevice.id, name: name, deviceClass: deviceClass, chargingStateAvailability: chargingStateAvailability, supportsWiredCharging: supportsWiredCharging, supportsWirelessCharging: supportsWirelessCharging, preferredChargingTransportMode: resolvedPreferredChargingTransportMode, wirelessChargingProfile: wirelessChargingProfile, configuredCompletionCurrents: configuredCompletionCurrents, notes: notes ) } else { didSave = appData.createChargedDevice( name: name, deviceClass: deviceClass, chargingStateAvailability: chargingStateAvailability, supportsWiredCharging: supportsWiredCharging, supportsWirelessCharging: supportsWirelessCharging, preferredChargingTransportMode: resolvedPreferredChargingTransportMode, wirelessChargingProfile: wirelessChargingProfile, configuredCompletionCurrents: configuredCompletionCurrents, notes: notes, meterMACAddress: meterMACAddress ) } if didSave { dismiss() } } .disabled( name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || (!supportsWiredCharging && !supportsWirelessCharging) || hasInvalidCompletionCurrentEntry ) } } } .navigationViewStyle(StackNavigationViewStyle()) .onChange(of: deviceClass) { newValue in applySuggestedChargingSupport(for: newValue) } .onAppear { guard chargedDevice == nil else { return } applySuggestedChargingSupport(for: deviceClass) } } private var editorTitle: String { if chargedDevice == nil { return deviceClass == .charger ? "New Charger" : "New Device" } return chargedDevice?.isCharger == true ? "Edit Charger" : "Edit Device" } private var saveButtonTitle: String { chargedDevice == nil ? "Save" : "Update" } private var supportedChargingModes: [ChargingTransportMode] { var modes: [ChargingTransportMode] = [] if supportsWiredCharging { modes.append(.wired) } if supportsWirelessCharging { modes.append(.wireless) } return modes } private var applicableSessionKinds: [ChargeSessionKind] { supportedChargingModes.flatMap { chargingTransportMode in chargingStateAvailability.supportedModes.map { chargingStateMode in ChargeSessionKind( chargingTransportMode: chargingTransportMode, chargingStateMode: chargingStateMode ) } } } private var parsedCompletionCurrents: [ChargeSessionKind: Double] { applicableSessionKinds.reduce(into: [ChargeSessionKind: Double]()) { result, sessionKind in guard let value = parsedOptionalCurrent(completionCurrentTexts[sessionKind] ?? "") else { return } result[sessionKind] = value } } private var hasInvalidCompletionCurrentEntry: Bool { applicableSessionKinds.contains { sessionKind in let text = completionCurrentTexts[sessionKind] ?? "" let normalized = text.trimmingCharacters(in: .whitespacesAndNewlines) return !normalized.isEmpty && parsedOptionalCurrent(text) == nil } } private var resolvedPreferredChargingTransportMode: ChargingTransportMode { if supportedChargingModes.contains(preferredChargingTransportMode) { return preferredChargingTransportMode } return supportsWiredCharging ? .wired : .wireless } private var preferredChargingTransportBinding: Binding { Binding( get: { resolvedPreferredChargingTransportMode }, set: { preferredChargingTransportMode = $0 } ) } private func completionCurrentTextBinding(for sessionKind: ChargeSessionKind) -> Binding { Binding( get: { completionCurrentTexts[sessionKind] ?? "" }, set: { completionCurrentTexts[sessionKind] = $0 } ) } private func applySuggestedChargingSupport(for deviceClass: ChargedDeviceClass) { if chargedDevice != nil { return } chargingStateAvailability = Self.suggestedChargingStateAvailability(for: deviceClass) switch deviceClass { case .iphone: supportsWiredCharging = true supportsWirelessCharging = true preferredChargingTransportMode = .wired case .watch: supportsWiredCharging = false supportsWirelessCharging = true preferredChargingTransportMode = .wireless case .powerbank: supportsWiredCharging = true supportsWirelessCharging = false preferredChargingTransportMode = .wired case .charger: supportsWiredCharging = true supportsWirelessCharging = true preferredChargingTransportMode = .wireless case .other: supportsWiredCharging = true supportsWirelessCharging = false preferredChargingTransportMode = .wired } } private func parsedOptionalCurrent(_ text: String) -> Double? { let normalized = text .trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: ",", with: ".") guard !normalized.isEmpty else { return nil } guard let value = Double(normalized), value > 0 else { return nil } return value } private static func makeCompletionCurrentTexts(for chargedDevice: ChargedDeviceSummary?) -> [ChargeSessionKind: String] { guard let chargedDevice else { return [:] } return ChargeSessionKind.allCases.reduce(into: [ChargeSessionKind: String]()) { result, sessionKind in result[sessionKind] = optionalCurrentText( chargedDevice.configuredCompletionCurrentAmps(for: sessionKind) ) } } private static func optionalCurrentText(_ value: Double?) -> String { guard let value else { return "" } return value.format(decimalDigits: 2) } private static func suggestedChargingStateAvailability(for deviceClass: ChargedDeviceClass) -> ChargingStateAvailability { switch deviceClass { case .iphone: return .onOrOff case .watch: return .onOnly case .powerbank: return .offOnly case .charger: return .onOnly case .other: return .onOnly } } }