USB-Meter / USB Meter / Views / ChargedDevices / ChargedDeviceEditorSheetView.swift
1 contributor
272 lines | 12.123kb
//
//  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 supportsChargingWhileOff: Bool
    @State private var supportsWiredCharging: Bool
    @State private var supportsWirelessCharging: Bool
    @State private var preferredChargingTransportMode: ChargingTransportMode
    @State private var wirelessChargingProfile: WirelessChargingProfile
    @State private var wiredChargeCompletionCurrentText: String
    @State private var wirelessChargeCompletionCurrentText: 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)
        _supportsChargingWhileOff = State(initialValue: chargedDevice?.supportsChargingWhileOff ?? false)
        _supportsWiredCharging = State(initialValue: chargedDevice?.supportsWiredCharging ?? true)
        _supportsWirelessCharging = State(initialValue: chargedDevice?.supportsWirelessCharging ?? true)
        _preferredChargingTransportMode = State(initialValue: chargedDevice?.preferredChargingTransportMode ?? .wired)
        _wirelessChargingProfile = State(initialValue: chargedDevice?.wirelessChargingProfile ?? .genericQi)
        _wiredChargeCompletionCurrentText = State(initialValue: Self.optionalCurrentText(chargedDevice?.wiredChargeCompletionCurrentAmps))
        _wirelessChargeCompletionCurrentText = State(initialValue: Self.optionalCurrentText(chargedDevice?.wirelessChargeCompletionCurrentAmps))
        _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")) {
                    Toggle("Can finish charging while device is off", isOn: $supportsChargingWhileOff)
                    Text("This flag is used when we decide which charge sessions are reliable enough to estimate battery capacity.")
                        .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 supportsWiredCharging || supportsWirelessCharging {
                        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 supportsWiredCharging {
                        TextField("Wired completion current (A)", text: $wiredChargeCompletionCurrentText)
                            .keyboardType(.decimalPad)
                        Text("Leave empty to keep learning this value from wired sessions.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }

                    if supportsWirelessCharging {
                        TextField("Wireless completion current (A)", text: $wirelessChargeCompletionCurrentText)
                            .keyboardType(.decimalPad)
                        Text("Leave empty to keep learning this value from wireless sessions.")
                            .font(.footnote)
                            .foregroundColor(.secondary)
                    }
                }

                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 didSave: Bool
                        if let chargedDevice {
                            didSave = appData.updateChargedDevice(
                                id: chargedDevice.id,
                                name: name,
                                deviceClass: deviceClass,
                                supportsChargingWhileOff: supportsChargingWhileOff,
                                supportsWiredCharging: supportsWiredCharging,
                                supportsWirelessCharging: supportsWirelessCharging,
                                preferredChargingTransportMode: resolvedPreferredChargingTransportMode,
                                wirelessChargingProfile: wirelessChargingProfile,
                                wiredChargeCompletionCurrentAmps: parsedOptionalCurrent(wiredChargeCompletionCurrentText),
                                wirelessChargeCompletionCurrentAmps: parsedOptionalCurrent(wirelessChargeCompletionCurrentText),
                                notes: notes
                            )
                        } else {
                            didSave = appData.createChargedDevice(
                                name: name,
                                deviceClass: deviceClass,
                                supportsChargingWhileOff: supportsChargingWhileOff,
                                supportsWiredCharging: supportsWiredCharging,
                                supportsWirelessCharging: supportsWirelessCharging,
                                preferredChargingTransportMode: resolvedPreferredChargingTransportMode,
                                wirelessChargingProfile: wirelessChargingProfile,
                                wiredChargeCompletionCurrentAmps: parsedOptionalCurrent(wiredChargeCompletionCurrentText),
                                wirelessChargeCompletionCurrentAmps: parsedOptionalCurrent(wirelessChargeCompletionCurrentText),
                                notes: notes,
                                meterMACAddress: meterMACAddress
                            )
                        }

                        if didSave {
                            dismiss()
                        }
                    }
                    .disabled(
                        name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                            || (!supportsWiredCharging && !supportsWirelessCharging)
                    )
                }
            }
        }
        .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 resolvedPreferredChargingTransportMode: ChargingTransportMode {
        if supportedChargingModes.contains(preferredChargingTransportMode) {
            return preferredChargingTransportMode
        }
        return supportsWiredCharging ? .wired : .wireless
    }

    private var preferredChargingTransportBinding: Binding<ChargingTransportMode> {
        Binding(
            get: { resolvedPreferredChargingTransportMode },
            set: { preferredChargingTransportMode = $0 }
        )
    }

    private func applySuggestedChargingSupport(for deviceClass: ChargedDeviceClass) {
        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
        }
        return Double(normalized)
    }

    private static func optionalCurrentText(_ value: Double?) -> String {
        guard let value else {
            return ""
        }
        return value.format(decimalDigits: 2)
    }
}