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 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)
}
}