Showing 4 changed files with 1111 additions and 6 deletions
+4 -4
USB Meter.xcodeproj/project.pbxproj
@@ -59,7 +59,7 @@
59 59
 		C10000033C8E4A7A00A10003 /* ChargedDeviceQRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000133C8E4A7A00A10013 /* ChargedDeviceQRCodeView.swift */; };
60 60
 		C10000043C8E4A7A00A10004 /* ChargedDeviceEditorSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */; };
61 61
 		C10000053C8E4A7A00A10005 /* ChargedDeviceLibrarySheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000153C8E4A7A00A10015 /* ChargedDeviceLibrarySheetView.swift */; };
62
-		C10000063C8E4A7A00A10006 /* ChargedDeviceDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */; };
62
+		C10000063C8E4A7A00A10006 /* ChargedDeviceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000163C8E4A7A00A10016 /* ChargedDeviceSettingsView.swift */; };
63 63
 		C10000073C8E4A7A00A10007 /* SidebarChargedDevicesSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */; };
64 64
 		C10000083C8E4A7A00A10008 /* BatteryCheckpointEditorSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */; };
65 65
 		C10000093C8E4A7A00A10009 /* CKModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C10000213C8E4A7A00A10021 /* CKModel.xcdatamodeld */; };
@@ -184,7 +184,7 @@
184 184
 		C10000133C8E4A7A00A10013 /* ChargedDeviceQRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceQRCodeView.swift; sourceTree = "<group>"; };
185 185
 		C10000143C8E4A7A00A10014 /* ChargedDeviceEditorSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceEditorSheetView.swift; sourceTree = "<group>"; };
186 186
 		C10000153C8E4A7A00A10015 /* ChargedDeviceLibrarySheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceLibrarySheetView.swift; sourceTree = "<group>"; };
187
-		C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceDetailView.swift; sourceTree = "<group>"; };
187
+		C10000163C8E4A7A00A10016 /* ChargedDeviceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChargedDeviceSettingsView.swift; sourceTree = "<group>"; };
188 188
 		C10000173C8E4A7A00A10017 /* SidebarChargedDevicesSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarChargedDevicesSectionView.swift; sourceTree = "<group>"; };
189 189
 		C10000183C8E4A7A00A10018 /* BatteryCheckpointEditorSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCheckpointEditorSheetView.swift; sourceTree = "<group>"; };
190 190
 		C10000193C8E4A7A00A10019 /* USB_Meter 4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "USB_Meter 4.xcdatamodel"; sourceTree = "<group>"; };
@@ -545,7 +545,7 @@
545 545
 		CD0000113FA0000000000011 /* Details */ = {
546 546
 			isa = PBXGroup;
547 547
 			children = (
548
-				C10000163C8E4A7A00A10016 /* ChargedDeviceDetailView.swift */,
548
+				C10000163C8E4A7A00A10016 /* ChargedDeviceSettingsView.swift */,
549 549
 			);
550 550
 			path = Details;
551 551
 			sourceTree = "<group>";
@@ -888,7 +888,7 @@
888 888
 				C10000033C8E4A7A00A10003 /* ChargedDeviceQRCodeView.swift in Sources */,
889 889
 				C10000043C8E4A7A00A10004 /* ChargedDeviceEditorSheetView.swift in Sources */,
890 890
 				C10000053C8E4A7A00A10005 /* ChargedDeviceLibrarySheetView.swift in Sources */,
891
-				C10000063C8E4A7A00A10006 /* ChargedDeviceDetailView.swift in Sources */,
891
+				C10000063C8E4A7A00A10006 /* ChargedDeviceSettingsView.swift in Sources */,
892 892
 				C100000A3C8E4A7A00A1000A /* ChargedDeviceSessionsView.swift in Sources */,
893 893
 				C100000B3C8E4A7A00A1000B /* ChargeSessionDetailView.swift in Sources */,
894 894
 				C1CCSS013C8E4A7A00CCSS01 /* ChargeSessionCompletionSheetView.swift in Sources */,
+1105 -0
USB Meter/Views/ChargedDevices/Details/ChargedDeviceSettingsView.swift
@@ -0,0 +1,1105 @@
1
+//
2
+//  ChargedDeviceSettingsView.swift
3
+//  USB Meter
4
+//
5
+//  Created by Codex on 10/04/2026.
6
+//
7
+
8
+import SwiftUI
9
+
10
+struct ChargedDeviceSettingsView: View {
11
+    private enum DetailTab: Hashable {
12
+        case overview
13
+        case standby
14
+        case sessions
15
+        case trends
16
+        case settings
17
+    }
18
+
19
+    @EnvironmentObject private var appData: AppData
20
+    @Environment(\.dismiss) private var dismiss
21
+
22
+    @State private var editorVisibility = false
23
+    @State private var deleteConfirmationVisibility = false
24
+    @State private var selectedTab: DetailTab = .overview
25
+    @State private var sessionSelectMode = false
26
+    @State private var selectedSessionIDs: Set<UUID> = []
27
+    @State private var pendingBatchDeletion = false
28
+
29
+    let chargedDeviceID: UUID
30
+
31
+    var body: some View {
32
+        Group {
33
+            if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
34
+                tabbedDetailView(chargedDevice)
35
+                .navigationTitle(chargedDevice.name)
36
+                .navigationBarTitleDisplayMode(.inline)
37
+            } else {
38
+                Text("This device is no longer available.")
39
+                    .foregroundColor(.secondary)
40
+                    .navigationTitle("Device")
41
+                    .navigationBarTitleDisplayMode(.inline)
42
+            }
43
+        }
44
+        .sidebarToggleToolbarItem()
45
+        .sheet(isPresented: $editorVisibility) {
46
+            if let chargedDevice = appData.chargedDeviceSummary(id: chargedDeviceID) {
47
+                if chargedDevice.isCharger {
48
+                    ChargerEditorSheetView(chargedDevice: chargedDevice)
49
+                        .environmentObject(appData)
50
+                } else {
51
+                    ChargedDeviceEditorSheetView(
52
+                        meterMACAddress: nil,
53
+                        chargedDevice: chargedDevice
54
+                    )
55
+                    .environmentObject(appData)
56
+                }
57
+            }
58
+        }
59
+        .confirmationDialog("Delete \(deletionTitle)?", isPresented: $deleteConfirmationVisibility, titleVisibility: .visible) {
60
+            Button("Delete", role: .destructive) {
61
+                if appData.deleteChargedDevice(id: chargedDeviceID) {
62
+                    dismiss()
63
+                }
64
+            }
65
+            Button("Cancel", role: .cancel) {}
66
+        } message: {
67
+            Text(deletionMessage)
68
+        }
69
+        .confirmationDialog(
70
+            "Delete \(selectedSessionIDs.count) Session\(selectedSessionIDs.count == 1 ? "" : "s")?",
71
+            isPresented: $pendingBatchDeletion,
72
+            titleVisibility: .visible
73
+        ) {
74
+            Button("Delete", role: .destructive, action: deleteSelectedSessions)
75
+            Button("Cancel", role: .cancel) {}
76
+        } message: {
77
+            Text("Deleting these sessions also recalculates capacity and every derived metric that used them.")
78
+        }
79
+    }
80
+
81
+    private func tabbedDetailView(_ chargedDevice: ChargedDeviceSummary) -> some View {
82
+        GeometryReader { proxy in
83
+            let tabs = availableTabs(for: chargedDevice)
84
+            let displayedTab = displayedTab(from: tabs)
85
+            let tabBarPresentation = AdaptiveTabBarPresentation.standard(for: proxy.size)
86
+
87
+            VStack(spacing: 0) {
88
+                ChargedDeviceDetailTabBarView(
89
+                    tabs: tabs,
90
+                    selection: $selectedTab,
91
+                    tint: tint(for: chargedDevice),
92
+                    presentation: tabBarPresentation,
93
+                    title: title(for:),
94
+                    systemImage: systemImage(for:)
95
+                )
96
+
97
+                Group {
98
+                    if displayedTab == .sessions {
99
+                        sessionsTabLayout(chargedDevice)
100
+                    } else {
101
+                        ScrollView {
102
+                            tabContent(displayedTab, chargedDevice: chargedDevice)
103
+                                .padding()
104
+                        }
105
+                    }
106
+                }
107
+                .id(displayedTab)
108
+                .transition(.opacity.combined(with: .move(edge: .trailing)))
109
+                .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
110
+            }
111
+            .animation(.easeInOut(duration: 0.22), value: displayedTab)
112
+            .animation(.easeInOut(duration: 0.22), value: tabs)
113
+            .onChange(of: selectedTab) { _ in
114
+                sessionSelectMode = false
115
+                selectedSessionIDs.removeAll()
116
+            }
117
+        }
118
+        .background(detailBackground(for: chargedDevice))
119
+        .onAppear {
120
+            ensureSelectedTabExists(for: chargedDevice)
121
+        }
122
+        .onChange(of: chargedDevice.isCharger) { _ in
123
+            ensureSelectedTabExists(for: chargedDevice)
124
+        }
125
+    }
126
+
127
+    @ViewBuilder
128
+    private func tabContent(_ tab: DetailTab, chargedDevice: ChargedDeviceSummary) -> some View {
129
+        VStack(spacing: 18) {
130
+            switch tab {
131
+            case .overview:
132
+                overviewTab(chargedDevice)
133
+            case .standby:
134
+                standbyTab(chargedDevice)
135
+            case .sessions:
136
+                sessionsTab(chargedDevice)
137
+            case .trends:
138
+                trendsTab(chargedDevice)
139
+            case .settings:
140
+                settingsTab(chargedDevice)
141
+            }
142
+        }
143
+    }
144
+
145
+    @ViewBuilder
146
+    private func overviewTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
147
+        headerCard(chargedDevice)
148
+        insightsCard(chargedDevice)
149
+
150
+        if let activeSession = chargedDevice.activeSession {
151
+            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
152
+        }
153
+    }
154
+
155
+    @ViewBuilder
156
+    private func standbyTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
157
+        standbyPowerCard(chargedDevice)
158
+    }
159
+
160
+    @ViewBuilder
161
+    private func sessionsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
162
+        if let activeSession = chargedDevice.activeSession {
163
+            activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice)
164
+        }
165
+
166
+        let sessions = closedSessions(for: chargedDevice)
167
+        if !sessions.isEmpty {
168
+            sessionListCard(sessions, chargedDevice: chargedDevice)
169
+        } else if chargedDevice.activeSession == nil {
170
+            emptyStateCard(
171
+                title: "No Sessions",
172
+                message: "Charging sessions will appear here after this device is used in a recording.",
173
+                tint: .teal
174
+            )
175
+        }
176
+    }
177
+
178
+    @ViewBuilder
179
+    private func trendsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
180
+        if !chargedDevice.capacityHistory.isEmpty {
181
+            capacityEvolutionCard(chargedDevice)
182
+        }
183
+
184
+        if !chargedDevice.typicalCurve.isEmpty {
185
+            typicalCurveCard(chargedDevice)
186
+        }
187
+
188
+        if chargedDevice.capacityHistory.isEmpty && chargedDevice.typicalCurve.isEmpty {
189
+            emptyStateCard(
190
+                title: "Learning Trends",
191
+                message: "Capacity history and charge curves will appear after enough completed sessions are available.",
192
+                tint: .blue
193
+            )
194
+        }
195
+    }
196
+
197
+    @ViewBuilder
198
+    private func settingsTab(_ chargedDevice: ChargedDeviceSummary) -> some View {
199
+        settingsCard(chargedDevice)
200
+    }
201
+
202
+    @ViewBuilder
203
+    private func sessionsTabLayout(_ chargedDevice: ChargedDeviceSummary) -> some View {
204
+        let allSessions = chargedDevice.sessions.sorted { lhs, rhs in
205
+            let lOpen = lhs.status.isOpen, rOpen = rhs.status.isOpen
206
+            if lOpen != rOpen { return lOpen }
207
+            return lhs.startedAt > rhs.startedAt
208
+        }
209
+        let totalEnergyWh = allSessions.reduce(0.0) { $0 + $1.effectiveOrMeasuredEnergyWh }
210
+        let totalDuration  = allSessions.reduce(0.0) { $0 + max($1.effectiveDuration, 0) }
211
+
212
+        VStack(spacing: 0) {
213
+            // Fixed non-scrolling header
214
+            VStack(spacing: 10) {
215
+                sessionsSummaryStrip(
216
+                    count: allSessions.count,
217
+                    totalEnergyWh: totalEnergyWh,
218
+                    totalDuration: totalDuration,
219
+                    hasActive: chargedDevice.activeSession != nil
220
+                )
221
+
222
+                if !allSessions.isEmpty {
223
+                    HStack(spacing: 12) {
224
+                        if sessionSelectMode && !selectedSessionIDs.isEmpty {
225
+                            Text("\(selectedSessionIDs.count) selected")
226
+                                .font(.subheadline)
227
+                                .foregroundColor(.secondary)
228
+                                .transition(.opacity.combined(with: .move(edge: .leading)))
229
+                        }
230
+                        Spacer()
231
+                        if sessionSelectMode && !selectedSessionIDs.isEmpty {
232
+                            Button {
233
+                                pendingBatchDeletion = true
234
+                            } label: {
235
+                                Image(systemName: "trash").foregroundColor(.red)
236
+                            }
237
+                            .transition(.opacity.combined(with: .scale))
238
+                        }
239
+                        Button(sessionSelectMode ? "Cancel" : "Select") {
240
+                            withAnimation(.easeInOut(duration: 0.2)) {
241
+                                sessionSelectMode.toggle()
242
+                                if !sessionSelectMode { selectedSessionIDs.removeAll() }
243
+                            }
244
+                        }
245
+                    }
246
+                    .animation(.easeInOut(duration: 0.2), value: sessionSelectMode)
247
+                    .animation(.easeInOut(duration: 0.2), value: selectedSessionIDs.isEmpty)
248
+                }
249
+            }
250
+            .padding()
251
+
252
+            // Scrollable session list
253
+            if allSessions.isEmpty {
254
+                emptyStateCard(
255
+                    title: "No Sessions",
256
+                    message: "Charging sessions will appear here after this device is used in a recording.",
257
+                    tint: .teal
258
+                )
259
+                .padding([.horizontal, .bottom])
260
+            } else {
261
+                ScrollView {
262
+                    VStack(spacing: 10) {
263
+                        ForEach(allSessions, id: \.id) { session in
264
+                            sessionListItem(session, chargedDevice: chargedDevice)
265
+                        }
266
+                    }
267
+                    .padding([.horizontal, .bottom])
268
+                }
269
+            }
270
+        }
271
+    }
272
+
273
+    private func sessionsSummaryStrip(
274
+        count: Int,
275
+        totalEnergyWh: Double,
276
+        totalDuration: TimeInterval,
277
+        hasActive: Bool
278
+    ) -> some View {
279
+        HStack(spacing: 0) {
280
+            summaryCell(value: "\(count)", label: count == 1 ? "session" : "sessions")
281
+            Divider().frame(height: 30)
282
+            summaryCell(value: "\(totalEnergyWh.format(decimalDigits: 2)) Wh", label: "energy")
283
+            Divider().frame(height: 30)
284
+            summaryCell(value: formatAccumulatedDuration(totalDuration), label: "duration")
285
+            if hasActive {
286
+                Divider().frame(height: 30)
287
+                HStack(spacing: 4) {
288
+                    Circle().fill(Color.green).frame(width: 6, height: 6)
289
+                    Text("Live")
290
+                        .font(.caption2.weight(.semibold))
291
+                        .foregroundColor(.green)
292
+                }
293
+                .frame(maxWidth: .infinity)
294
+            }
295
+        }
296
+        .padding(.vertical, 8)
297
+        .padding(.horizontal, 12)
298
+        .meterCard(tint: .teal, fillOpacity: 0.08, strokeOpacity: 0.14, cornerRadius: 14)
299
+    }
300
+
301
+    private func summaryCell(value: String, label: String) -> some View {
302
+        VStack(spacing: 2) {
303
+            Text(value)
304
+                .font(.subheadline.weight(.bold))
305
+                .foregroundColor(.primary)
306
+                .monospacedDigit()
307
+                .lineLimit(1)
308
+                .minimumScaleFactor(0.7)
309
+            Text(label)
310
+                .font(.caption2)
311
+                .foregroundColor(.secondary)
312
+        }
313
+        .frame(maxWidth: .infinity)
314
+    }
315
+
316
+    private func formatAccumulatedDuration(_ duration: TimeInterval) -> String {
317
+        let formatter = DateComponentsFormatter()
318
+        formatter.allowedUnits = duration >= 3600 ? [.hour, .minute] : [.minute, .second]
319
+        formatter.unitsStyle = .abbreviated
320
+        formatter.zeroFormattingBehavior = .dropAll
321
+        return formatter.string(from: duration) ?? "0m"
322
+    }
323
+
324
+    private func headerCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
325
+        HStack(alignment: .top, spacing: 18) {
326
+            ChargedDeviceQRCodeView(qrIdentifier: chargedDevice.qrIdentifier, side: 118)
327
+
328
+            VStack(alignment: .leading, spacing: 10) {
329
+                ChargedDeviceIdentityLabelView(
330
+                    chargedDevice: chargedDevice,
331
+                    iconPointSize: 22
332
+                )
333
+                .font(.title3.weight(.bold))
334
+
335
+                Text(chargedDevice.identityTitle)
336
+                    .font(.subheadline.weight(.semibold))
337
+                    .foregroundColor(.secondary)
338
+
339
+                if let meterMAC = chargedDevice.lastAssociatedMeterMAC {
340
+                    Text("Default meter: \(meterMAC)")
341
+                        .font(.caption)
342
+                        .foregroundColor(.secondary)
343
+                }
344
+
345
+                Text(chargedDevice.qrIdentifier)
346
+                    .font(.caption2.monospaced())
347
+                    .foregroundColor(.secondary)
348
+                    .textSelection(.enabled)
349
+            }
350
+
351
+            Spacer(minLength: 0)
352
+        }
353
+        .frame(maxWidth: .infinity, alignment: .leading)
354
+        .padding(18)
355
+        .meterCard(tint: tint(for: chargedDevice), fillOpacity: 0.20, strokeOpacity: 0.26, cornerRadius: 20)
356
+    }
357
+
358
+    private func settingsCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
359
+        MeterInfoCardView(title: "Settings", tint: tint(for: chargedDevice)) {
360
+            MeterInfoRowView(
361
+                label: "Kind",
362
+                value: chargedDevice.isCharger ? "Charger" : chargedDevice.deviceClass.title
363
+            )
364
+            MeterInfoRowView(label: "Template", value: chargedDevice.identityTitle)
365
+            MeterInfoRowView(label: "QR ID", value: chargedDevice.qrIdentifier)
366
+
367
+            if let meterMAC = chargedDevice.lastAssociatedMeterMAC {
368
+                MeterInfoRowView(label: "Default Meter", value: meterMAC)
369
+            }
370
+
371
+            MeterInfoRowView(label: "Created", value: chargedDevice.createdAt.format())
372
+            MeterInfoRowView(label: "Updated", value: chargedDevice.updatedAt.format())
373
+
374
+            Divider()
375
+
376
+            Button(action: showEditor) {
377
+                Label("Edit \(chargedDevice.isCharger ? "Charger" : "Device")", systemImage: "pencil")
378
+                    .font(.subheadline.weight(.semibold))
379
+                    .frame(maxWidth: .infinity)
380
+                    .padding(.vertical, 10)
381
+                    .meterCard(tint: tint(for: chargedDevice), fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
382
+            }
383
+            .buttonStyle(.plain)
384
+
385
+            Button(role: .destructive, action: showDeleteConfirmation) {
386
+                Label("Delete \(chargedDevice.isCharger ? "Charger" : "Device")", systemImage: "trash")
387
+                    .font(.subheadline.weight(.semibold))
388
+                    .frame(maxWidth: .infinity)
389
+                    .padding(.vertical, 10)
390
+                    .meterCard(tint: .red, fillOpacity: 0.10, strokeOpacity: 0.18, cornerRadius: 14)
391
+            }
392
+            .buttonStyle(.plain)
393
+        }
394
+    }
395
+
396
+    private func emptyStateCard(title: String, message: String, tint: Color) -> some View {
397
+        VStack(alignment: .leading, spacing: 8) {
398
+            Text(title)
399
+                .font(.headline)
400
+            Text(message)
401
+                .font(.footnote)
402
+                .foregroundColor(.secondary)
403
+                .fixedSize(horizontal: false, vertical: true)
404
+        }
405
+        .frame(maxWidth: .infinity, alignment: .leading)
406
+        .padding(18)
407
+        .meterCard(tint: tint, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 18)
408
+    }
409
+
410
+    private func insightsCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
411
+        MeterInfoCardView(title: "Insights", tint: tint(for: chargedDevice)) {
412
+            if chargedDevice.isCharger {
413
+                chargerInsights(chargedDevice)
414
+            } else {
415
+                deviceInsights(chargedDevice)
416
+            }
417
+
418
+            if let notes = chargedDevice.notes, !notes.isEmpty {
419
+                Divider()
420
+                Text(notes)
421
+                    .font(.footnote)
422
+                    .foregroundColor(.secondary)
423
+                    .frame(maxWidth: .infinity, alignment: .leading)
424
+            }
425
+        }
426
+    }
427
+
428
+    @ViewBuilder
429
+    private func deviceInsights(_ chargedDevice: ChargedDeviceSummary) -> some View {
430
+        if chargedDevice.hasMultipleChargingStateModes {
431
+            MeterInfoRowView(
432
+                label: "Charge Modes",
433
+                value: chargedDevice.chargingStateAvailability.title
434
+            )
435
+        }
436
+        if chargedDevice.hasMultipleChargingTransports {
437
+            MeterInfoRowView(
438
+                label: "Charging Support",
439
+                value: chargedDevice.supportedChargingModes.map(\.title).joined(separator: " + ")
440
+            )
441
+        }
442
+        if chargedDevice.showsWirelessProfileDetails {
443
+            MeterInfoRowView(
444
+                label: "Wireless Profile",
445
+                value: chargedDevice.wirelessChargingProfile.title
446
+            )
447
+        }
448
+
449
+        ForEach(completionSessionKinds(for: chargedDevice), id: \.rawValue) { sessionKind in
450
+            MeterInfoRowView(
451
+                label: completionCurrentLabel(for: chargedDevice, sessionKind: sessionKind),
452
+                value: completionCurrentDescription(for: chargedDevice, sessionKind: sessionKind)
453
+            )
454
+        }
455
+        MeterInfoRowView(
456
+            label: "Estimated Capacity",
457
+            value: chargedDevice.estimatedBatteryCapacityWh.map { "\($0.format(decimalDigits: 2)) Wh" } ?? "Not enough data"
458
+        )
459
+        if let wiredCapacity = chargedDevice.wiredEstimatedBatteryCapacityWh {
460
+            if chargedDevice.hasMultipleChargingTransports {
461
+                MeterInfoRowView(
462
+                    label: "Wired Capacity",
463
+                    value: "\(wiredCapacity.format(decimalDigits: 2)) Wh"
464
+                )
465
+            }
466
+        }
467
+        if let wirelessCapacity = chargedDevice.wirelessEstimatedBatteryCapacityWh {
468
+            if chargedDevice.hasMultipleChargingTransports {
469
+                MeterInfoRowView(
470
+                    label: "Wireless Capacity",
471
+                    value: "\(wirelessCapacity.format(decimalDigits: 2)) Wh"
472
+                )
473
+            }
474
+        }
475
+        if let wirelessEfficiencyFactor = chargedDevice.wirelessChargerEfficiencyFactor,
476
+           chargedDevice.showsWirelessProfileDetails {
477
+            MeterInfoRowView(
478
+                label: "Wireless Efficiency",
479
+                value: "\(Int((wirelessEfficiencyFactor * 100).rounded()))%"
480
+            )
481
+        }
482
+        MeterInfoRowView(
483
+            label: "Charge Sessions",
484
+            value: "\(chargedDevice.sessionCount)"
485
+        )
486
+    }
487
+
488
+    @ViewBuilder
489
+    private func chargerInsights(_ chargedDevice: ChargedDeviceSummary) -> some View {
490
+        if let chargerType = chargedDevice.chargerType {
491
+            MeterInfoRowView(
492
+                label: "Type",
493
+                value: chargerType.title
494
+            )
495
+        }
496
+        if !chargedDevice.chargerObservedVoltageSelections.isEmpty {
497
+            MeterInfoRowView(
498
+                label: "Observed Voltages",
499
+                value: chargedDevice.chargerObservedVoltageSelections
500
+                    .map { "\($0.format(decimalDigits: 1)) V" }
501
+                    .joined(separator: ", ")
502
+            )
503
+        }
504
+        if let chargerIdleCurrentAmps = chargedDevice.chargerIdleCurrentAmps {
505
+            MeterInfoRowView(
506
+                label: "Idle Current",
507
+                value: "\(chargerIdleCurrentAmps.format(decimalDigits: 2)) A"
508
+            )
509
+        }
510
+        if let chargerEfficiencyFactor = chargedDevice.chargerEfficiencyFactor {
511
+            MeterInfoRowView(
512
+                label: "Efficiency",
513
+                value: "\(Int((chargerEfficiencyFactor * 100).rounded()))%"
514
+            )
515
+        }
516
+        if let chargerMaximumPowerWatts = chargedDevice.chargerMaximumPowerWatts {
517
+            MeterInfoRowView(
518
+                label: "Max Power",
519
+                value: "\(chargerMaximumPowerWatts.format(decimalDigits: 2)) W"
520
+            )
521
+        }
522
+        if let latestStandbyPowerMeasurement = chargedDevice.latestStandbyPowerMeasurement {
523
+            MeterInfoRowView(
524
+                label: "Standby Power",
525
+                value: "\(latestStandbyPowerMeasurement.averagePowerWatts.format(decimalDigits: 3)) W"
526
+            )
527
+            MeterInfoRowView(
528
+                label: "Standby Projection",
529
+                value: standbyEnergyLabel(latestStandbyPowerMeasurement.projectedYearlyEnergyWh) + " / year"
530
+            )
531
+        }
532
+        MeterInfoRowView(
533
+            label: "Wireless Sessions",
534
+            value: "\(chargedDevice.sessionCount)"
535
+        )
536
+
537
+    }
538
+
539
+    private func standbyPowerCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
540
+        let latestMeasurement = chargedDevice.latestStandbyPowerMeasurement
541
+
542
+        return MeterInfoCardView(
543
+            title: "Standby Power",
544
+            tint: .orange
545
+        ) {
546
+            if standbyMeasurementMeters.isEmpty {
547
+                Text("Connect a meter first. Standby measurement is launched from a live meter feed, so only currently available meters can be selected here.")
548
+                    .font(.footnote)
549
+                    .foregroundColor(.secondary)
550
+                    .frame(maxWidth: .infinity, alignment: .leading)
551
+            } else {
552
+                NavigationLink(
553
+                    destination: ChargerStandbyPowerWizardView(
554
+                        preferredChargerID: chargedDevice.id,
555
+                        locksChargerSelection: true
556
+                    )
557
+                ) {
558
+                    Label("New Measurement", systemImage: "plus.circle.fill")
559
+                        .font(.subheadline.weight(.semibold))
560
+                        .foregroundColor(.orange)
561
+                }
562
+                .buttonStyle(.plain)
563
+            }
564
+
565
+            if let latestMeasurement {
566
+                Divider()
567
+
568
+                NavigationLink(
569
+                    destination: ChargerStandbyPowerMeasurementDetailView(
570
+                        chargerID: chargedDevice.id,
571
+                        measurementID: latestMeasurement.id
572
+                    )
573
+                ) {
574
+                    VStack(alignment: .leading, spacing: 8) {
575
+                        HStack {
576
+                            Text("Latest Measurement")
577
+                                .font(.subheadline.weight(.semibold))
578
+                                .foregroundColor(.primary)
579
+                            Spacer()
580
+                            Text("\(latestMeasurement.averagePowerWatts.format(decimalDigits: 3)) W")
581
+                                .font(.subheadline.weight(.bold))
582
+                                .foregroundColor(.primary)
583
+                                .monospacedDigit()
584
+                        }
585
+
586
+                        Text(
587
+                            "\(latestMeasurement.endedAt.format()) • \(latestMeasurement.sampleCount) samples • \(standbyEnergyLabel(latestMeasurement.projectedYearlyEnergyWh)) / year"
588
+                        )
589
+                        .font(.caption)
590
+                        .foregroundColor(.secondary)
591
+                    }
592
+                }
593
+                .buttonStyle(.plain)
594
+            }
595
+
596
+            if chargedDevice.standbyPowerMeasurements.isEmpty == false {
597
+                Divider()
598
+
599
+                NavigationLink(
600
+                    destination: ChargerStandbyPowerMeasurementsView(chargerID: chargedDevice.id)
601
+                ) {
602
+                    Label("View Saved Measurements", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
603
+                        .font(.subheadline.weight(.semibold))
604
+                        .foregroundColor(.blue)
605
+                }
606
+                .buttonStyle(.plain)
607
+            }
608
+        }
609
+    }
610
+
611
+    private func activeSessionSummaryCard(
612
+        _ activeSession: ChargeSessionSummary,
613
+        chargedDevice: ChargedDeviceSummary
614
+    ) -> some View {
615
+        NavigationLink(
616
+            destination: ChargeSessionDetailView(
617
+                chargedDeviceID: chargedDevice.id,
618
+                sessionID: activeSession.id
619
+            )
620
+        ) {
621
+            VStack(alignment: .leading, spacing: 14) {
622
+                HStack(alignment: .firstTextBaseline) {
623
+                    VStack(alignment: .leading, spacing: 4) {
624
+                        Text("Current Session")
625
+                            .font(.headline)
626
+                            .foregroundColor(.primary)
627
+                        Text(activeSession.status.title)
628
+                            .font(.caption.weight(.semibold))
629
+                            .foregroundColor(statusTint(for: activeSession))
630
+                    }
631
+
632
+                    Spacer()
633
+
634
+                    Image(systemName: "chevron.right")
635
+                        .font(.caption.weight(.semibold))
636
+                        .foregroundColor(.secondary)
637
+                }
638
+
639
+                LazyVGrid(columns: activeSessionSummaryColumns, spacing: 8) {
640
+                    activeSessionMetricCell(
641
+                        label: "Energy",
642
+                        value: "\(activeSession.effectiveOrMeasuredEnergyWh.format(decimalDigits: 2)) Wh",
643
+                        tint: .teal
644
+                    )
645
+                    activeSessionMetricCell(
646
+                        label: "Duration",
647
+                        value: sessionDurationText(activeSession),
648
+                        tint: .orange
649
+                    )
650
+                    if let maximumObservedPowerWatts = activeSession.maximumObservedPowerWatts {
651
+                        activeSessionMetricCell(
652
+                            label: "Max Power",
653
+                            value: "\(maximumObservedPowerWatts.format(decimalDigits: 2)) W",
654
+                            tint: .blue
655
+                        )
656
+                    }
657
+                    if let batteryPrediction = chargedDevice.batteryLevelPrediction(for: activeSession) {
658
+                        activeSessionMetricCell(
659
+                            label: "Battery",
660
+                            value: "\(batteryPrediction.predictedPercent.format(decimalDigits: 0))%",
661
+                            tint: .green
662
+                        )
663
+                    } else if let targetBatteryPercent = activeSession.targetBatteryPercent {
664
+                        activeSessionMetricCell(
665
+                            label: "Target",
666
+                            value: "\(targetBatteryPercent.format(decimalDigits: 0))%",
667
+                            tint: .indigo
668
+                        )
669
+                    }
670
+                }
671
+
672
+                Text("Started \(activeSession.startedAt.format())")
673
+                    .font(.caption)
674
+                    .foregroundColor(.secondary)
675
+            }
676
+        }
677
+        .buttonStyle(.plain)
678
+        .padding(18)
679
+        .meterCard(tint: statusTint(for: activeSession), fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 18)
680
+    }
681
+
682
+    private var activeSessionSummaryColumns: [GridItem] {
683
+        [
684
+            GridItem(.flexible(minimum: 92), spacing: 8),
685
+            GridItem(.flexible(minimum: 92), spacing: 8)
686
+        ]
687
+    }
688
+
689
+    private func activeSessionMetricCell(label: String, value: String, tint: Color) -> some View {
690
+        VStack(alignment: .leading, spacing: 4) {
691
+            Text(label)
692
+                .font(.caption2)
693
+                .foregroundColor(.secondary)
694
+            Text(value)
695
+                .font(.footnote.weight(.semibold))
696
+                .foregroundColor(.primary)
697
+                .monospacedDigit()
698
+                .lineLimit(1)
699
+                .minimumScaleFactor(0.8)
700
+        }
701
+        .frame(maxWidth: .infinity, alignment: .leading)
702
+        .padding(10)
703
+        .meterCard(tint: tint, fillOpacity: 0.08, strokeOpacity: 0.12, cornerRadius: 12)
704
+    }
705
+
706
+    private func capacityEvolutionCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
707
+        VStack(alignment: .leading, spacing: 12) {
708
+            Text("Capacity Evolution")
709
+                .font(.headline)
710
+
711
+            ForEach(chargedDevice.capacityHistory.suffix(6)) { point in
712
+                HStack {
713
+                    Text(point.timestamp.format())
714
+                        .font(.caption)
715
+                        .foregroundColor(.secondary)
716
+                    Spacer()
717
+                    if chargedDevice.shouldShowChargingTransport(point.chargingTransportMode) {
718
+                        Text(point.chargingTransportMode.title)
719
+                            .font(.caption2)
720
+                            .foregroundColor(.secondary)
721
+                        Text("•")
722
+                            .foregroundColor(.secondary)
723
+                    }
724
+                    Text("\(point.capacityWh.format(decimalDigits: 2)) Wh")
725
+                        .font(.footnote.weight(.semibold))
726
+                }
727
+            }
728
+        }
729
+        .frame(maxWidth: .infinity, alignment: .leading)
730
+        .padding(18)
731
+        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18)
732
+    }
733
+
734
+    private func typicalCurveCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
735
+        VStack(alignment: .leading, spacing: 12) {
736
+            Text("Typical Charge Curve")
737
+                .font(.headline)
738
+
739
+            ForEach(chargedDevice.typicalCurve) { point in
740
+                HStack {
741
+                    Text("\(point.percentBin)%")
742
+                        .font(.footnote.weight(.semibold))
743
+                    Spacer()
744
+                    Text("\(point.averageEnergyWh.format(decimalDigits: 2)) Wh")
745
+                        .font(.caption.weight(.semibold))
746
+                    Text("•")
747
+                        .foregroundColor(.secondary)
748
+                    Text("\(point.sampleCount) sample\(point.sampleCount == 1 ? "" : "s")")
749
+                        .font(.caption2)
750
+                        .foregroundColor(.secondary)
751
+                }
752
+            }
753
+        }
754
+        .frame(maxWidth: .infinity, alignment: .leading)
755
+        .padding(18)
756
+        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18)
757
+    }
758
+
759
+    private func sessionListCard(
760
+        _ sessions: [ChargeSessionSummary],
761
+        chargedDevice: ChargedDeviceSummary
762
+    ) -> some View {
763
+        let totalEnergyWh = sessions.reduce(0) { $0 + $1.effectiveOrMeasuredEnergyWh }
764
+        let completedCount = sessions.filter { $0.status == .completed }.count
765
+        let sortedSessions = sessions.sorted { $0.startedAt > $1.startedAt }
766
+
767
+        return VStack(alignment: .leading, spacing: 14) {
768
+            MeterInfoCardView(title: "Closed Sessions", tint: .teal) {
769
+                MeterInfoRowView(label: "Sessions", value: "\(sessions.count)")
770
+                MeterInfoRowView(label: "Completed", value: "\(completedCount)")
771
+                MeterInfoRowView(label: "Total Energy", value: "\(totalEnergyWh.format(decimalDigits: 2)) Wh")
772
+            }
773
+
774
+            HStack(spacing: 12) {
775
+                if sessionSelectMode && !selectedSessionIDs.isEmpty {
776
+                    Text("\(selectedSessionIDs.count) selected")
777
+                        .font(.subheadline)
778
+                        .foregroundColor(.secondary)
779
+                        .transition(.opacity.combined(with: .move(edge: .leading)))
780
+                }
781
+                Spacer()
782
+                if sessionSelectMode && !selectedSessionIDs.isEmpty {
783
+                    Button {
784
+                        pendingBatchDeletion = true
785
+                    } label: {
786
+                        Image(systemName: "trash")
787
+                            .foregroundColor(.red)
788
+                    }
789
+                    .transition(.opacity.combined(with: .scale))
790
+                }
791
+                Button(sessionSelectMode ? "Cancel" : "Select") {
792
+                    withAnimation(.easeInOut(duration: 0.2)) {
793
+                        sessionSelectMode.toggle()
794
+                        if !sessionSelectMode { selectedSessionIDs.removeAll() }
795
+                    }
796
+                }
797
+            }
798
+            .animation(.easeInOut(duration: 0.2), value: sessionSelectMode)
799
+            .animation(.easeInOut(duration: 0.2), value: selectedSessionIDs.isEmpty)
800
+
801
+            VStack(spacing: 10) {
802
+                ForEach(sortedSessions, id: \.id) { session in
803
+                    sessionListItem(session, chargedDevice: chargedDevice)
804
+                }
805
+            }
806
+        }
807
+    }
808
+
809
+    private func sessionListItem(
810
+        _ session: ChargeSessionSummary,
811
+        chargedDevice: ChargedDeviceSummary
812
+    ) -> some View {
813
+        let sessionTint = statusTint(for: session)
814
+        let isOpen = session.status.isOpen
815
+        let isSelected = selectedSessionIDs.contains(session.id)
816
+
817
+        return Group {
818
+            if sessionSelectMode && !isOpen {
819
+                Button {
820
+                    withAnimation(.easeInOut(duration: 0.15)) {
821
+                        if isSelected { selectedSessionIDs.remove(session.id) }
822
+                        else          { selectedSessionIDs.insert(session.id) }
823
+                    }
824
+                } label: {
825
+                    sessionRowContent(session, sessionTint: sessionTint, isOpen: isOpen, isSelected: isSelected)
826
+                }
827
+                .buttonStyle(.plain)
828
+            } else {
829
+                NavigationLink(
830
+                    destination: ChargeSessionDetailView(
831
+                        chargedDeviceID: chargedDevice.id,
832
+                        sessionID: session.id
833
+                    )
834
+                ) {
835
+                    sessionRowContent(session, sessionTint: sessionTint, isOpen: isOpen, isSelected: false)
836
+                }
837
+                .buttonStyle(.plain)
838
+            }
839
+        }
840
+    }
841
+
842
+    private func sessionRowContent(
843
+        _ session: ChargeSessionSummary,
844
+        sessionTint: Color,
845
+        isOpen: Bool,
846
+        isSelected: Bool
847
+    ) -> some View {
848
+        VStack(alignment: .leading, spacing: 10) {
849
+            HStack(alignment: .firstTextBaseline, spacing: 10) {
850
+                if sessionSelectMode {
851
+                    Group {
852
+                        if isOpen {
853
+                            Image(systemName: "minus.circle")
854
+                                .foregroundColor(.secondary.opacity(0.35))
855
+                        } else {
856
+                            Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
857
+                                .foregroundColor(isSelected ? .teal : .secondary)
858
+                        }
859
+                    }
860
+                    .font(.body)
861
+                    .transition(.opacity)
862
+                }
863
+
864
+                VStack(alignment: .leading, spacing: 2) {
865
+                    Text(session.startedAt.format())
866
+                        .font(.subheadline.weight(.semibold))
867
+                    Text(session.status.title)
868
+                        .font(.caption2)
869
+                        .foregroundColor(sessionTint)
870
+                }
871
+
872
+                Spacer()
873
+
874
+                VStack(alignment: .trailing, spacing: 2) {
875
+                    Text("\(session.effectiveOrMeasuredEnergyWh.format(decimalDigits: 2)) Wh")
876
+                        .font(.subheadline.weight(.semibold))
877
+                        .foregroundColor(.primary)
878
+                    Text(sessionDurationText(session))
879
+                        .font(.caption)
880
+                        .foregroundColor(.secondary)
881
+                }
882
+            }
883
+
884
+            Divider()
885
+
886
+            HStack(spacing: 8) {
887
+                if let batteryDelta = session.batteryDeltaPercent {
888
+                    Label("\(batteryDelta >= 0 ? "+" : "")\(Int(batteryDelta.rounded()))% charged", systemImage: "battery.100percent")
889
+                        .font(.caption2)
890
+                        .foregroundColor(.secondary)
891
+                }
892
+
893
+                if let capacityWh = session.capacityEstimateWh {
894
+                    Text("est. \(capacityWh.format(decimalDigits: 1)) Wh")
895
+                        .font(.caption2)
896
+                        .foregroundColor(.secondary)
897
+                }
898
+
899
+                Spacer()
900
+
901
+                if !session.displayedAggregatedSamples.isEmpty {
902
+                    Label("\(session.displayedAggregatedSamples.count) points", systemImage: "chart.xyaxis.line")
903
+                        .font(.caption2)
904
+                        .foregroundColor(.secondary)
905
+                }
906
+            }
907
+        }
908
+        .padding(12)
909
+        .meterCard(
910
+            tint: sessionTint,
911
+            fillOpacity: isSelected ? 0.16 : (isOpen ? 0.14 : 0.08),
912
+            strokeOpacity: isSelected ? 0.22 : (isOpen ? 0.30 : 0.14),
913
+            cornerRadius: 14
914
+        )
915
+    }
916
+
917
+    private func closedSessions(for chargedDevice: ChargedDeviceSummary) -> [ChargeSessionSummary] {
918
+        chargedDevice.sessions.filter { !$0.status.isOpen }
919
+    }
920
+
921
+    private func availableTabs(for chargedDevice: ChargedDeviceSummary) -> [DetailTab] {
922
+        if chargedDevice.isCharger {
923
+            return [.overview, .standby, .settings]
924
+        }
925
+        return [.overview, .sessions, .trends, .settings]
926
+    }
927
+
928
+    private func displayedTab(from tabs: [DetailTab]) -> DetailTab {
929
+        if tabs.contains(selectedTab) {
930
+            return selectedTab
931
+        }
932
+        return tabs.first ?? .overview
933
+    }
934
+
935
+    private func ensureSelectedTabExists(for chargedDevice: ChargedDeviceSummary) {
936
+        let tabs = availableTabs(for: chargedDevice)
937
+        if !tabs.contains(selectedTab) {
938
+            selectedTab = tabs.first ?? .overview
939
+        }
940
+    }
941
+
942
+    private func title(for tab: DetailTab) -> String {
943
+        switch tab {
944
+        case .overview:
945
+            return "Overview"
946
+        case .standby:
947
+            return "Standby"
948
+        case .sessions:
949
+            return "Sessions"
950
+        case .trends:
951
+            return "Trends"
952
+        case .settings:
953
+            return "Settings"
954
+        }
955
+    }
956
+
957
+    private func systemImage(for tab: DetailTab) -> String {
958
+        switch tab {
959
+        case .overview:
960
+            return "house.fill"
961
+        case .standby:
962
+            return "bolt.badge.clock"
963
+        case .sessions:
964
+            return "clock.arrow.trianglehead.counterclockwise.rotate.90"
965
+        case .trends:
966
+            return "chart.xyaxis.line"
967
+        case .settings:
968
+            return "gearshape.fill"
969
+        }
970
+    }
971
+
972
+    private func detailBackground(for chargedDevice: ChargedDeviceSummary) -> some View {
973
+        LinearGradient(
974
+            colors: [tint(for: chargedDevice).opacity(0.18), Color.clear],
975
+            startPoint: .topLeading,
976
+            endPoint: .bottomTrailing
977
+        )
978
+        .ignoresSafeArea()
979
+    }
980
+
981
+    private func tint(for chargedDevice: ChargedDeviceSummary) -> Color {
982
+        switch chargedDevice.deviceClass {
983
+        case .iphone:
984
+            return .blue
985
+        case .watch:
986
+            return .green
987
+        case .powerbank:
988
+            return .orange
989
+        case .charger:
990
+            return .pink
991
+        case .other:
992
+            return .secondary
993
+        }
994
+    }
995
+
996
+    private func statusTint(for session: ChargeSessionSummary) -> Color {
997
+        switch session.status {
998
+        case .active:
999
+            return .green
1000
+        case .paused:
1001
+            return .orange
1002
+        case .completed:
1003
+            return .teal
1004
+        case .abandoned:
1005
+            return .secondary
1006
+        }
1007
+    }
1008
+
1009
+    private func sessionDurationText(_ session: ChargeSessionSummary) -> String {
1010
+        let formatter = DateComponentsFormatter()
1011
+        let effectiveDuration = max(session.effectiveDuration, 0)
1012
+        formatter.allowedUnits = effectiveDuration >= 3600 ? [.hour, .minute] : [.minute, .second]
1013
+        formatter.unitsStyle = .abbreviated
1014
+        formatter.zeroFormattingBehavior = .dropAll
1015
+        return formatter.string(from: effectiveDuration) ?? "0m"
1016
+    }
1017
+
1018
+    private func standbyEnergyLabel(_ wattHours: Double) -> String {
1019
+        if wattHours >= 1000 {
1020
+            return "\((wattHours / 1000).format(decimalDigits: 3)) kWh"
1021
+        }
1022
+        return "\(wattHours.format(decimalDigits: 2)) Wh"
1023
+    }
1024
+
1025
+    private var standbyMeasurementMeters: [AppData.MeterSummary] {
1026
+        appData.meterSummaries.filter { $0.meter != nil }
1027
+    }
1028
+
1029
+    private func completionCurrentDescription(
1030
+        for chargedDevice: ChargedDeviceSummary,
1031
+        sessionKind: ChargeSessionKind
1032
+    ) -> String {
1033
+        if let configuredCurrent = chargedDevice.configuredCompletionCurrentAmps(for: sessionKind) {
1034
+            if let learnedCurrent = chargedDevice.learnedCompletionCurrentAmps(for: sessionKind),
1035
+               abs(configuredCurrent - learnedCurrent) >= 0.01 {
1036
+                return "\(configuredCurrent.format(decimalDigits: 2)) A configured • \(learnedCurrent.format(decimalDigits: 2)) A learned"
1037
+            }
1038
+            return "\(configuredCurrent.format(decimalDigits: 2)) A configured"
1039
+        }
1040
+
1041
+        if let learnedCurrent = chargedDevice.learnedCompletionCurrentAmps(for: sessionKind) {
1042
+            return "\(learnedCurrent.format(decimalDigits: 2)) A learned"
1043
+        }
1044
+
1045
+        return "Learning"
1046
+    }
1047
+
1048
+    private func completionCurrentLabel(
1049
+        for chargedDevice: ChargedDeviceSummary,
1050
+        sessionKind: ChargeSessionKind
1051
+    ) -> String {
1052
+        let showsTransport = chargedDevice.shouldShowChargingTransport(sessionKind.chargingTransportMode)
1053
+        let showsState = chargedDevice.shouldShowChargingStateMode(sessionKind.chargingStateMode)
1054
+
1055
+        switch (showsTransport, showsState) {
1056
+        case (true, true):
1057
+            return "\(sessionKind.shortTitle) Stop Current"
1058
+        case (true, false):
1059
+            return "\(sessionKind.chargingTransportMode.title) Stop Current"
1060
+        case (false, true):
1061
+            return "\(sessionKind.chargingStateMode.title) Stop Current"
1062
+        case (false, false):
1063
+            return "Stop Current"
1064
+        }
1065
+    }
1066
+
1067
+    private func completionSessionKinds(for chargedDevice: ChargedDeviceSummary) -> [ChargeSessionKind] {
1068
+        chargedDevice.supportedChargingModes.flatMap { chargingTransportMode in
1069
+            chargedDevice.supportedChargingStateModes.map { chargingStateMode in
1070
+                ChargeSessionKind(
1071
+                    chargingTransportMode: chargingTransportMode,
1072
+                    chargingStateMode: chargingStateMode
1073
+                )
1074
+            }
1075
+        }
1076
+    }
1077
+
1078
+    private func deleteSelectedSessions() {
1079
+        for id in selectedSessionIDs {
1080
+            _ = appData.deleteChargeSession(sessionID: id)
1081
+        }
1082
+        selectedSessionIDs.removeAll()
1083
+        sessionSelectMode = false
1084
+    }
1085
+
1086
+    private func showEditor() {
1087
+        editorVisibility = true
1088
+    }
1089
+
1090
+    private func showDeleteConfirmation() {
1091
+        deleteConfirmationVisibility = true
1092
+    }
1093
+
1094
+    private var deletionTitle: String {
1095
+        appData.chargedDeviceSummary(id: chargedDeviceID)?.isCharger == true ? "charger" : "device"
1096
+    }
1097
+
1098
+    private var deletionMessage: String {
1099
+        if appData.chargedDeviceSummary(id: chargedDeviceID)?.isCharger == true {
1100
+            return "This removes the charger from the library and unlinks it from wireless sessions that used it."
1101
+        }
1102
+        return "This removes the device and its stored charging history from the library."
1103
+    }
1104
+
1105
+}
+1 -1
USB Meter/Views/ChargedDevices/Sidebar/SidebarChargedDeviceLibraryView.swift
@@ -73,7 +73,7 @@ struct SidebarChargedDeviceLibraryView: View {
73 73
 
74 74
     private var deviceRows: some View {
75 75
         ForEach(displayedDevices) { device in
76
-            NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: device.id)) {
76
+            NavigationLink(destination: ChargedDeviceSettingsView(chargedDeviceID: device.id)) {
77 77
                 ChargedDeviceLibraryRowView(chargedDevice: device, isSelected: false)
78 78
             }
79 79
             .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+1 -1
USB Meter/Views/ChargedDevices/Sidebar/SidebarChargedDevicesSectionView.swift
@@ -28,7 +28,7 @@ struct SidebarChargedDevicesSectionView: View {
28 28
                 .transition(.opacity.combined(with: .move(edge: .top)))
29 29
 
30 30
                 ForEach(chargedDevices) { chargedDevice in
31
-                    NavigationLink(destination: ChargedDeviceDetailView(chargedDeviceID: chargedDevice.id)) {
31
+                    NavigationLink(destination: ChargedDeviceSettingsView(chargedDeviceID: chargedDevice.id)) {
32 32
                         ChargedDeviceSidebarCardView(chargedDevice: chargedDevice)
33 33
                     }
34 34
                     .buttonStyle(.plain)