1 contributor
//
// 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<ChargingTransportMode> {
Binding(
get: { resolvedPreferredChargingTransportMode },
set: { preferredChargingTransportMode = $0 }
)
}
private func completionCurrentTextBinding(for sessionKind: ChargeSessionKind) -> Binding<String> {
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
}
}
}