USB-Meter / USB Meter / Views / ChargedDevices / ChargedDeviceEditorSheetView.swift
Newer Older
272 lines | 12.123kb
Bogdan Timofte authored a month ago
1
//
2
//  ChargedDeviceEditorSheetView.swift
3
//  USB Meter
4
//
5
//  Created by Codex on 10/04/2026.
6
//
7

            
8
import SwiftUI
9

            
10
struct ChargedDeviceEditorSheetView: View {
11
    @EnvironmentObject private var appData: AppData
12
    @Environment(\.dismiss) private var dismiss
13

            
14
    let meterMACAddress: String?
15
    let chargedDevice: ChargedDeviceSummary?
16
    let suggestedDeviceClass: ChargedDeviceClass?
17

            
18
    @State private var name: String
19
    @State private var deviceClass: ChargedDeviceClass
20
    @State private var supportsChargingWhileOff: Bool
21
    @State private var supportsWiredCharging: Bool
22
    @State private var supportsWirelessCharging: Bool
23
    @State private var preferredChargingTransportMode: ChargingTransportMode
24
    @State private var wirelessChargingProfile: WirelessChargingProfile
25
    @State private var wiredChargeCompletionCurrentText: String
26
    @State private var wirelessChargeCompletionCurrentText: String
27
    @State private var notes: String
28

            
29
    init(
30
        meterMACAddress: String?,
31
        chargedDevice: ChargedDeviceSummary? = nil,
32
        suggestedDeviceClass: ChargedDeviceClass? = nil
33
    ) {
34
        self.meterMACAddress = meterMACAddress
35
        self.chargedDevice = chargedDevice
36
        self.suggestedDeviceClass = suggestedDeviceClass
37
        let initialDeviceClass = chargedDevice?.deviceClass ?? suggestedDeviceClass ?? .iphone
38
        _name = State(initialValue: chargedDevice?.name ?? "")
39
        _deviceClass = State(initialValue: initialDeviceClass)
40
        _supportsChargingWhileOff = State(initialValue: chargedDevice?.supportsChargingWhileOff ?? false)
41
        _supportsWiredCharging = State(initialValue: chargedDevice?.supportsWiredCharging ?? true)
42
        _supportsWirelessCharging = State(initialValue: chargedDevice?.supportsWirelessCharging ?? true)
43
        _preferredChargingTransportMode = State(initialValue: chargedDevice?.preferredChargingTransportMode ?? .wired)
44
        _wirelessChargingProfile = State(initialValue: chargedDevice?.wirelessChargingProfile ?? .genericQi)
45
        _wiredChargeCompletionCurrentText = State(initialValue: Self.optionalCurrentText(chargedDevice?.wiredChargeCompletionCurrentAmps))
46
        _wirelessChargeCompletionCurrentText = State(initialValue: Self.optionalCurrentText(chargedDevice?.wirelessChargeCompletionCurrentAmps))
47
        _notes = State(initialValue: chargedDevice?.notes ?? "")
48
    }
49

            
50
    var body: some View {
51
        NavigationView {
52
            Form {
53
                Section(header: Text("Identity")) {
54
                    TextField("Name", text: $name)
55

            
56
                    Picker("Class", selection: $deviceClass) {
57
                        ForEach(ChargedDeviceClass.allCases) { deviceClass in
58
                            Label(deviceClass.title, systemImage: deviceClass.symbolName)
59
                                .tag(deviceClass)
60
                        }
61
                    }
62

            
63
                    if let chargedDevice {
64
                        Text(chargedDevice.qrIdentifier)
65
                            .font(.caption.monospaced())
66
                            .foregroundColor(.secondary)
67
                            .textSelection(.enabled)
68
                    }
69
                }
70

            
71
                Section(header: Text("Charge Behaviour")) {
72
                    Toggle("Can finish charging while device is off", isOn: $supportsChargingWhileOff)
73
                    Text("This flag is used when we decide which charge sessions are reliable enough to estimate battery capacity.")
74
                        .font(.footnote)
75
                        .foregroundColor(.secondary)
76
                }
77

            
78
                Section(header: Text("Charging Support")) {
79
                    Toggle("Supports wired charging", isOn: $supportsWiredCharging)
80
                    Toggle("Supports wireless charging", isOn: $supportsWirelessCharging)
81

            
82
                    if supportsWirelessCharging {
83
                        Picker("Wireless profile", selection: $wirelessChargingProfile) {
84
                            ForEach(WirelessChargingProfile.allCases) { profile in
85
                                Text(profile.title)
86
                                    .tag(profile)
87
                            }
88
                        }
89

            
90
                        Text(wirelessChargingProfile.description)
91
                            .font(.footnote)
92
                            .foregroundColor(.secondary)
93
                    }
94

            
95
                    if supportsWiredCharging || supportsWirelessCharging {
96
                        Picker("Default session type", selection: preferredChargingTransportBinding) {
97
                            ForEach(supportedChargingModes) { chargingTransportMode in
98
                                Label(chargingTransportMode.title, systemImage: chargingTransportMode.symbolName)
99
                                    .tag(chargingTransportMode)
100
                            }
101
                        }
102
                    } else {
103
                        Text("Enable at least one charging method.")
104
                            .font(.footnote)
105
                            .foregroundColor(.secondary)
106
                    }
107
                }
108

            
109
                Section(header: Text("Charge Completion")) {
110
                    if supportsWiredCharging {
111
                        TextField("Wired completion current (A)", text: $wiredChargeCompletionCurrentText)
112
                            .keyboardType(.decimalPad)
113
                        Text("Leave empty to keep learning this value from wired sessions.")
114
                            .font(.footnote)
115
                            .foregroundColor(.secondary)
116
                    }
117

            
118
                    if supportsWirelessCharging {
119
                        TextField("Wireless completion current (A)", text: $wirelessChargeCompletionCurrentText)
120
                            .keyboardType(.decimalPad)
121
                        Text("Leave empty to keep learning this value from wireless sessions.")
122
                            .font(.footnote)
123
                            .foregroundColor(.secondary)
124
                    }
125
                }
126

            
127
                Section(header: Text("Notes")) {
128
                    TextField("Optional notes", text: $notes)
129
                }
130
            }
131
            .navigationTitle(editorTitle)
132
            .navigationBarTitleDisplayMode(.inline)
133
            .toolbar {
134
                ToolbarItem(placement: .cancellationAction) {
135
                    Button("Cancel") {
136
                        dismiss()
137
                    }
138
                }
139
                ToolbarItem(placement: .confirmationAction) {
140
                    Button(saveButtonTitle) {
141
                        let didSave: Bool
142
                        if let chargedDevice {
143
                            didSave = appData.updateChargedDevice(
144
                                id: chargedDevice.id,
145
                                name: name,
146
                                deviceClass: deviceClass,
147
                                supportsChargingWhileOff: supportsChargingWhileOff,
148
                                supportsWiredCharging: supportsWiredCharging,
149
                                supportsWirelessCharging: supportsWirelessCharging,
150
                                preferredChargingTransportMode: resolvedPreferredChargingTransportMode,
151
                                wirelessChargingProfile: wirelessChargingProfile,
152
                                wiredChargeCompletionCurrentAmps: parsedOptionalCurrent(wiredChargeCompletionCurrentText),
153
                                wirelessChargeCompletionCurrentAmps: parsedOptionalCurrent(wirelessChargeCompletionCurrentText),
154
                                notes: notes
155
                            )
156
                        } else {
157
                            didSave = appData.createChargedDevice(
158
                                name: name,
159
                                deviceClass: deviceClass,
160
                                supportsChargingWhileOff: supportsChargingWhileOff,
161
                                supportsWiredCharging: supportsWiredCharging,
162
                                supportsWirelessCharging: supportsWirelessCharging,
163
                                preferredChargingTransportMode: resolvedPreferredChargingTransportMode,
164
                                wirelessChargingProfile: wirelessChargingProfile,
165
                                wiredChargeCompletionCurrentAmps: parsedOptionalCurrent(wiredChargeCompletionCurrentText),
166
                                wirelessChargeCompletionCurrentAmps: parsedOptionalCurrent(wirelessChargeCompletionCurrentText),
167
                                notes: notes,
168
                                meterMACAddress: meterMACAddress
169
                            )
170
                        }
171

            
172
                        if didSave {
173
                            dismiss()
174
                        }
175
                    }
176
                    .disabled(
177
                        name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
178
                            || (!supportsWiredCharging && !supportsWirelessCharging)
179
                    )
180
                }
181
            }
182
        }
183
        .navigationViewStyle(StackNavigationViewStyle())
184
        .onChange(of: deviceClass) { newValue in
185
            applySuggestedChargingSupport(for: newValue)
186
        }
187
        .onAppear {
188
            guard chargedDevice == nil else {
189
                return
190
            }
191
            applySuggestedChargingSupport(for: deviceClass)
192
        }
193
    }
194

            
195
    private var editorTitle: String {
196
        if chargedDevice == nil {
197
            return deviceClass == .charger ? "New Charger" : "New Device"
198
        }
199
        return chargedDevice?.isCharger == true ? "Edit Charger" : "Edit Device"
200
    }
201

            
202
    private var saveButtonTitle: String {
203
        chargedDevice == nil ? "Save" : "Update"
204
    }
205

            
206
    private var supportedChargingModes: [ChargingTransportMode] {
207
        var modes: [ChargingTransportMode] = []
208
        if supportsWiredCharging {
209
            modes.append(.wired)
210
        }
211
        if supportsWirelessCharging {
212
            modes.append(.wireless)
213
        }
214
        return modes
215
    }
216

            
217
    private var resolvedPreferredChargingTransportMode: ChargingTransportMode {
218
        if supportedChargingModes.contains(preferredChargingTransportMode) {
219
            return preferredChargingTransportMode
220
        }
221
        return supportsWiredCharging ? .wired : .wireless
222
    }
223

            
224
    private var preferredChargingTransportBinding: Binding<ChargingTransportMode> {
225
        Binding(
226
            get: { resolvedPreferredChargingTransportMode },
227
            set: { preferredChargingTransportMode = $0 }
228
        )
229
    }
230

            
231
    private func applySuggestedChargingSupport(for deviceClass: ChargedDeviceClass) {
232
        switch deviceClass {
233
        case .iphone:
234
            supportsWiredCharging = true
235
            supportsWirelessCharging = true
236
            preferredChargingTransportMode = .wired
237
        case .watch:
238
            supportsWiredCharging = false
239
            supportsWirelessCharging = true
240
            preferredChargingTransportMode = .wireless
241
        case .powerbank:
242
            supportsWiredCharging = true
243
            supportsWirelessCharging = false
244
            preferredChargingTransportMode = .wired
245
        case .charger:
246
            supportsWiredCharging = true
247
            supportsWirelessCharging = true
248
            preferredChargingTransportMode = .wireless
249
        case .other:
250
            supportsWiredCharging = true
251
            supportsWirelessCharging = false
252
            preferredChargingTransportMode = .wired
253
        }
254
    }
255

            
256
    private func parsedOptionalCurrent(_ text: String) -> Double? {
257
        let normalized = text
258
            .trimmingCharacters(in: .whitespacesAndNewlines)
259
            .replacingOccurrences(of: ",", with: ".")
260
        guard !normalized.isEmpty else {
261
            return nil
262
        }
263
        return Double(normalized)
264
    }
265

            
266
    private static func optionalCurrentText(_ value: Double?) -> String {
267
        guard let value else {
268
            return ""
269
        }
270
        return value.format(decimalDigits: 2)
271
    }
272
}