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

            
8
import SwiftUI
9

            
10
enum ChargedDeviceLibraryMode {
11
    case device
12
    case charger
13

            
Bogdan Timofte authored a month ago
14
    var kind: ChargedDeviceKind {
Bogdan Timofte authored a month ago
15
        switch self {
16
        case .device:
Bogdan Timofte authored a month ago
17
            return .device
Bogdan Timofte authored a month ago
18
        case .charger:
Bogdan Timofte authored a month ago
19
            return .charger
Bogdan Timofte authored a month ago
20
        }
21
    }
22

            
Bogdan Timofte authored a month ago
23
    var title: String {
Bogdan Timofte authored a month ago
24
        switch self {
25
        case .device:
Bogdan Timofte authored a month ago
26
            return "Devices"
Bogdan Timofte authored a month ago
27
        case .charger:
Bogdan Timofte authored a month ago
28
            return "Chargers"
Bogdan Timofte authored a month ago
29
        }
30
    }
31

            
Bogdan Timofte authored a month ago
32
    var singularTitle: String {
Bogdan Timofte authored a month ago
33
        switch self {
34
        case .device:
Bogdan Timofte authored a month ago
35
            return "Device"
Bogdan Timofte authored a month ago
36
        case .charger:
Bogdan Timofte authored a month ago
37
            return "Charger"
Bogdan Timofte authored a month ago
38
        }
39
    }
40
}
41

            
42
struct ChargedDeviceLibrarySheetView: View {
43
    @EnvironmentObject private var appData: AppData
44

            
45
    @Binding var visibility: Bool
46

            
47
    let meterMACAddress: String
48
    let meterTint: Color
49
    let mode: ChargedDeviceLibraryMode
50

            
51
    @State private var editorVisibility = false
52
    @State private var editingChargedDevice: ChargedDeviceSummary?
53

            
54
    var body: some View {
55
        NavigationView {
56
            List {
57
                if displayedChargedDevices.isEmpty {
58
                    VStack(alignment: .leading, spacing: 10) {
Bogdan Timofte authored a month ago
59
                        HStack(spacing: 8) {
60
                            Text("No \(mode.title.lowercased()) yet.")
61
                                .font(.headline)
62
                            ContextInfoButton(
63
                                title: mode.title,
64
                                message: emptyStateDescription
65
                            )
66
                        }
Bogdan Timofte authored a month ago
67
                    }
68
                    .padding(.vertical, 10)
69
                    .listRowBackground(Color.clear)
70
                } else {
71
                    ForEach(displayedChargedDevices) { chargedDevice in
72
                        Button {
73
                            select(chargedDevice)
74
                            visibility = false
75
                        } label: {
76
                            ChargedDeviceLibraryRowView(
77
                                chargedDevice: chargedDevice,
78
                                isSelected: chargedDevice.id == selectedDeviceID
79
                            )
80
                        }
81
                        .buttonStyle(.plain)
82
                        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
83
                            Button {
84
                                editingChargedDevice = chargedDevice
85
                            } label: {
86
                                Label("Edit", systemImage: "pencil")
87
                            }
88
                            .tint(.blue)
89
                        }
90
                        .contextMenu {
91
                            Button {
92
                                editingChargedDevice = chargedDevice
93
                            } label: {
Bogdan Timofte authored a month ago
94
                                Label("Edit \(mode.singularTitle)", systemImage: "pencil")
Bogdan Timofte authored a month ago
95
                            }
96
                        }
97
                    }
98
                }
99
            }
100
            .listStyle(InsetGroupedListStyle())
101
            .background(
102
                LinearGradient(
103
                    colors: [meterTint.opacity(0.14), Color.clear],
104
                    startPoint: .topLeading,
105
                    endPoint: .bottomTrailing
106
                )
107
                .ignoresSafeArea()
108
            )
109
            .navigationTitle(mode.title)
110
            .navigationBarTitleDisplayMode(.inline)
111
            .toolbar {
112
                ToolbarItem(placement: .cancellationAction) {
113
                    Button("Done") {
114
                        visibility = false
115
                    }
116
                }
117
                ToolbarItem(placement: .confirmationAction) {
118
                    Button("New") {
119
                        editorVisibility = true
120
                    }
121
                }
122
            }
123
        }
124
        .navigationViewStyle(StackNavigationViewStyle())
125
        .sheet(isPresented: $editorVisibility) {
126
            ChargedDeviceEditorSheetView(
127
                meterMACAddress: meterMACAddress,
Bogdan Timofte authored a month ago
128
                kind: mode.kind
Bogdan Timofte authored a month ago
129
            )
130
                .environmentObject(appData)
131
        }
132
        .sheet(item: $editingChargedDevice) { chargedDevice in
133
            ChargedDeviceEditorSheetView(
134
                meterMACAddress: nil,
Bogdan Timofte authored a month ago
135
                kind: mode.kind,
136
                chargedDevice: chargedDevice
Bogdan Timofte authored a month ago
137
            )
138
            .environmentObject(appData)
139
        }
140
    }
141

            
142
    private var displayedChargedDevices: [ChargedDeviceSummary] {
143
        switch mode {
144
        case .device:
145
            return appData.deviceSummaries
146
        case .charger:
147
            return appData.chargerSummaries
148
        }
149
    }
150

            
151
    private var selectedDeviceID: UUID? {
152
        switch mode {
153
        case .device:
154
            return appData.currentChargedDeviceSummary(for: meterMACAddress)?.id
155
        case .charger:
156
            return appData.currentChargerSummary(for: meterMACAddress)?.id
157
        }
158
    }
159

            
160
    private var emptyStateDescription: String {
161
        switch mode {
162
        case .device:
163
            return "Create one here, then select it before or during a charging session. The selected device becomes the default for this meter."
164
        case .charger:
165
            return "Create one here, then select it for wireless charging sessions. The selected charger becomes the default wireless source for this meter."
166
        }
167
    }
168

            
169
    private func select(_ chargedDevice: ChargedDeviceSummary) {
170
        switch mode {
171
        case .device:
172
            appData.assignChargedDevice(chargedDevice.id, to: meterMACAddress)
173
        case .charger:
174
            appData.assignCharger(chargedDevice.id, to: meterMACAddress)
175
        }
176
    }
177
}
178

            
179
private struct ChargedDeviceLibraryRowView: View {
180
    let chargedDevice: ChargedDeviceSummary
181
    let isSelected: Bool
182

            
183
    var body: some View {
184
        HStack(alignment: .top, spacing: 14) {
185
            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 58)
186

            
187
            VStack(alignment: .leading, spacing: 6) {
188
                HStack {
Bogdan Timofte authored a month ago
189
                    Label(chargedDevice.name, systemImage: chargedDevice.identitySymbolName)
Bogdan Timofte authored a month ago
190
                        .font(.headline)
191
                        .foregroundColor(.primary)
192
                    Spacer()
193
                    if isSelected {
194
                        Image(systemName: "checkmark.circle.fill")
195
                            .foregroundColor(.green)
196
                    }
197
                }
198

            
Bogdan Timofte authored a month ago
199
                Text(chargedDevice.identityTitle)
Bogdan Timofte authored a month ago
200
                    .font(.caption.weight(.semibold))
201
                    .foregroundColor(.secondary)
202

            
Bogdan Timofte authored a month ago
203
                if chargedDevice.isCharger {
204
                    if !chargedDevice.chargerObservedVoltageSelections.isEmpty {
205
                        Text(
206
                            chargedDevice.chargerObservedVoltageSelections
207
                                .map { "\($0.format(decimalDigits: 1)) V" }
208
                                .joined(separator: ", ")
209
                        )
210
                        .font(.caption2)
Bogdan Timofte authored a month ago
211
                        .foregroundColor(.secondary)
Bogdan Timofte authored a month ago
212
                    } else if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
213
                        Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
214
                            .font(.caption2)
215
                            .foregroundColor(.secondary)
216
                    } else {
217
                        Text("Wireless charger")
218
                            .font(.caption2)
219
                            .foregroundColor(.secondary)
220
                    }
221
                } else {
222
                    Text(chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + "))
Bogdan Timofte authored a month ago
223
                        .font(.caption2)
224
                        .foregroundColor(.secondary)
Bogdan Timofte authored a month ago
225

            
226
                    if let capacity = chargedDevice.estimatedBatteryCapacityWh {
227
                        Text("Estimated capacity: \(capacity.format(decimalDigits: 2)) Wh")
228
                            .font(.caption)
229
                            .foregroundColor(.secondary)
230
                    }
231

            
232
                    if let minimumCurrent = chargedDevice.minimumCurrentAmps {
233
                        Text("Completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
234
                            .font(.caption2)
235
                            .foregroundColor(.secondary)
236
                    }
Bogdan Timofte authored a month ago
237
                }
238
            }
239
        }
240
        .padding(.vertical, 4)
241
    }
242
}