// // 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 kind: ChargedDeviceKind @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 wirelessChargingProfile: WirelessChargingProfile @State private var completionCurrentTexts: [ChargeSessionKind: String] @State private var notes: String init( meterMACAddress: String?, kind: ChargedDeviceKind, chargedDevice: ChargedDeviceSummary? = nil ) { self.meterMACAddress = meterMACAddress self.chargedDevice = chargedDevice let resolvedKind = chargedDevice?.kind ?? kind self.kind = resolvedKind let initialDeviceClass = chargedDevice?.deviceClass ?? (resolvedKind == .charger ? .charger : .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) _wirelessChargingProfile = State(initialValue: chargedDevice?.wirelessChargingProfile ?? .genericQi) _completionCurrentTexts = State(initialValue: Self.makeCompletionCurrentTexts(for: chargedDevice)) _notes = State(initialValue: chargedDevice?.notes ?? "") } var body: some View { NavigationView { Form { identitySection if kind == .device { deviceChargeBehaviourSection deviceChargingSupportSection deviceCompletionSection } else { chargerInformationSection } notesSection } .navigationTitle(editorTitle) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(saveButtonTitle) { save() } .disabled(!canSave) } } } .navigationViewStyle(StackNavigationViewStyle()) .onChange(of: deviceClass) { newValue in guard kind == .device else { return } applySuggestedChargingSupport(for: newValue) } .onAppear { guard kind == .device, chargedDevice == nil else { return } applySuggestedChargingSupport(for: deviceClass) } } private var identitySection: some View { Section(header: Text("Identity")) { TextField(kind == .charger ? "Charger name" : "Name", text: $name) if kind == .device { Picker("Class", selection: $deviceClass) { ForEach(ChargedDeviceClass.deviceCases) { deviceClass in Label(deviceClass.title, systemImage: deviceClass.symbolName) .tag(deviceClass) } } } if let chargedDevice { Text(chargedDevice.qrIdentifier) .font(.caption.monospaced()) .foregroundColor(.secondary) .textSelection(.enabled) } } } private var deviceChargeBehaviourSection: some View { Section( header: ContextInfoHeader( title: "Charge Behaviour", message: "Choose whether sessions for this device are recorded only while it is on, only while it is off, or explicitly in either state." ) ) { Picker("Session Modes", selection: $chargingStateAvailability) { ForEach(ChargingStateAvailability.allCases) { availability in Text(availability.title) .tag(availability) } } } } private var deviceChargingSupportSection: some View { Section( header: ContextInfoHeader( title: "Charging Support", message: "Enable the charging methods this device actually supports. Wireless devices can also keep a dedicated profile so learning stays separate." ) ) { 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) } } } if supportedChargingModes.isEmpty { Text("Enable at least one charging method.") .font(.footnote) .foregroundColor(.secondary) } } } private var deviceCompletionSection: some View { Section( header: ContextInfoHeader( title: "Charge Completion", message: "Completion currents can be set per session type. Leave a value empty to keep learning that threshold automatically from sessions of the same type." ) ) { 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) } .padding(.vertical, 2) } } } } private var chargerInformationSection: some View { Section( header: ContextInfoHeader( title: "Charger", message: "Chargers are edited separately from devices. Their charge-session metrics are learned automatically from wireless sessions." ) ) { EmptyView() } } private var notesSection: some View { Section(header: Text("Notes")) { TextField("Optional notes", text: $notes) } } private var editorTitle: String { if chargedDevice == nil { return "New \(kind.title)" } return "Edit \(kind.title)" } private var saveButtonTitle: String { chargedDevice == nil ? "Save" : "Update" } private var canSave: Bool { let hasValidName = name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false guard kind == .device else { return hasValidName } return hasValidName && (supportsWiredCharging || supportsWirelessCharging) && !hasInvalidCompletionCurrentEntry } 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 func completionCurrentTextBinding(for sessionKind: ChargeSessionKind) -> Binding { Binding( get: { completionCurrentTexts[sessionKind] ?? "" }, set: { completionCurrentTexts[sessionKind] = $0 } ) } private func save() { let didSave: Bool if kind == .charger { if let chargedDevice { didSave = appData.updateCharger( id: chargedDevice.id, name: name, notes: notes ) } else { didSave = appData.createCharger( name: name, notes: notes, meterMACAddress: meterMACAddress ) } } else { let configuredCompletionCurrents = parsedCompletionCurrents if let chargedDevice { didSave = appData.updateDevice( id: chargedDevice.id, name: name, deviceClass: deviceClass, chargingStateAvailability: chargingStateAvailability, supportsWiredCharging: supportsWiredCharging, supportsWirelessCharging: supportsWirelessCharging, wirelessChargingProfile: wirelessChargingProfile, configuredCompletionCurrents: configuredCompletionCurrents, notes: notes ) } else { didSave = appData.createDevice( name: name, deviceClass: deviceClass, chargingStateAvailability: chargingStateAvailability, supportsWiredCharging: supportsWiredCharging, supportsWirelessCharging: supportsWirelessCharging, wirelessChargingProfile: wirelessChargingProfile, configuredCompletionCurrents: configuredCompletionCurrents, notes: notes, meterMACAddress: meterMACAddress ) } } if didSave { dismiss() } } private func applySuggestedChargingSupport(for deviceClass: ChargedDeviceClass) { if chargedDevice != nil { return } chargingStateAvailability = Self.suggestedChargingStateAvailability(for: deviceClass) switch deviceClass { case .iphone: supportsWiredCharging = true supportsWirelessCharging = true case .watch: supportsWiredCharging = false supportsWirelessCharging = true case .powerbank: supportsWiredCharging = true supportsWirelessCharging = false case .charger: supportsWiredCharging = false supportsWirelessCharging = true case .other: supportsWiredCharging = true supportsWirelessCharging = false } } 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, .other: return .onOnly } } }