Newer Older
581 lines | 27.85kb
Bogdan Timofte authored 2 months ago
1
//
Bogdan Timofte authored 2 months ago
2
//  ChargeRecordSheetView.swift
Bogdan Timofte authored 2 months ago
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 09/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import SwiftUI
10

            
Bogdan Timofte authored 2 months ago
11
struct ChargeRecordSheetView: View {
Bogdan Timofte authored 2 months ago
12

            
Bogdan Timofte authored 2 months ago
13
    @Binding var visibility: Bool
Bogdan Timofte authored a month ago
14
    @EnvironmentObject private var appData: AppData
Bogdan Timofte authored 2 months ago
15
    @EnvironmentObject private var usbMeter: Meter
Bogdan Timofte authored a month ago
16
    @State private var chargedDeviceLibraryVisibility = false
17
    @State private var chargerLibraryVisibility = false
18
    @State private var checkpointEditorVisibility = false
19
    @State private var editingChargedDevice: ChargedDeviceSummary?
20
    @State private var targetNotificationEditorVisibility = false
Bogdan Timofte authored 2 months ago
21

            
22
    var body: some View {
Bogdan Timofte authored 2 months ago
23
        NavigationView {
24
            ScrollView {
25
                VStack(spacing: 16) {
Bogdan Timofte authored 2 months ago
26
                    VStack(alignment: .leading, spacing: 8) {
27
                        HStack {
28
                            Text("Charge Record")
29
                                .font(.system(.title3, design: .rounded).weight(.bold))
30
                            Spacer()
31
                            Text(usbMeter.chargeRecordStatusText)
32
                                .font(.caption.weight(.bold))
33
                                .foregroundColor(usbMeter.chargeRecordStatusColor)
34
                                .padding(.horizontal, 10)
35
                                .padding(.vertical, 6)
36
                                .meterCard(
37
                                    tint: usbMeter.chargeRecordStatusColor,
38
                                    fillOpacity: 0.18,
39
                                    strokeOpacity: 0.24,
40
                                    cornerRadius: 999
41
                                )
42
                        }
43
                        Text("App-side charge accumulation based on the stop-threshold workflow.")
44
                            .font(.footnote)
45
                            .foregroundColor(.secondary)
Bogdan Timofte authored 2 months ago
46
                    }
47
                    .frame(maxWidth: .infinity)
Bogdan Timofte authored 2 months ago
48
                    .padding(18)
49
                    .meterCard(tint: .pink, fillOpacity: 0.18, strokeOpacity: 0.24)
Bogdan Timofte authored 2 months ago
50

            
Bogdan Timofte authored a month ago
51
                    chargedDeviceSection
52

            
53
                    if let activeChargeSession {
54
                        chargeMonitorSection(activeChargeSession)
55
                    }
56

            
Bogdan Timofte authored 2 months ago
57
                    ChargeRecordMetricsTableView(
Bogdan Timofte authored 2 months ago
58
                        labels: ["Capacity", "Energy", "Duration", "Stop Threshold"],
59
                        values: [
60
                            "\(usbMeter.chargeRecordAH.format(decimalDigits: 3)) Ah",
61
                            "\(usbMeter.chargeRecordWH.format(decimalDigits: 3)) Wh",
62
                            usbMeter.chargeRecordDurationDescription,
63
                            "\(usbMeter.chargeRecordStopThreshold.format(decimalDigits: 2)) A"
64
                        ]
65
                    )
Bogdan Timofte authored 2 months ago
66
                    .padding(18)
67
                    .meterCard(tint: .pink, fillOpacity: 0.14, strokeOpacity: 0.20)
Bogdan Timofte authored 2 months ago
68

            
Bogdan Timofte authored 2 months ago
69
                    if usbMeter.chargeRecordTimeRange != nil {
70
                        VStack(alignment: .leading, spacing: 12) {
71
                            HStack {
72
                                Text("Charge Curve")
Bogdan Timofte authored 2 months ago
73
                                    .font(.headline)
Bogdan Timofte authored 2 months ago
74
                                Spacer()
75
                                Button("Reset Graph") {
76
                                    usbMeter.resetChargeRecordGraph()
77
                                }
Bogdan Timofte authored 2 months ago
78
                                .foregroundColor(.red)
Bogdan Timofte authored 2 months ago
79
                            }
Bogdan Timofte authored 2 months ago
80
                            MeasurementChartView(timeRange: usbMeter.chargeRecordTimeRange)
81
                                .environmentObject(usbMeter.measurements)
82
                                .frame(minHeight: 220)
83
                            Text("Reset Graph clears the current charge-record session and removes older shared samples that are no longer needed for this curve.")
84
                                .font(.footnote)
85
                                .foregroundColor(.secondary)
Bogdan Timofte authored 2 months ago
86
                        }
Bogdan Timofte authored 2 months ago
87
                        .padding(18)
88
                        .meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20)
Bogdan Timofte authored 2 months ago
89
                    }
90

            
91
                    VStack(alignment: .leading, spacing: 12) {
92
                        Text("Stop Threshold")
Bogdan Timofte authored 2 months ago
93
                            .font(.headline)
Bogdan Timofte authored 2 months ago
94
                        Slider(value: $usbMeter.chargeRecordStopThreshold, in: 0...0.30, step: 0.01)
95
                        Text("The app starts accumulating when current rises above this threshold and stops when it falls back to or below it.")
Bogdan Timofte authored 2 months ago
96
                            .font(.footnote)
97
                            .foregroundColor(.secondary)
Bogdan Timofte authored 2 months ago
98
                        Button("Reset") {
99
                            usbMeter.resetChargeRecord()
100
                        }
101
                        .frame(maxWidth: .infinity)
Bogdan Timofte authored 2 months ago
102
                        .padding(.vertical, 10)
103
                        .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
104
                        .buttonStyle(.plain)
Bogdan Timofte authored 2 months ago
105
                    }
Bogdan Timofte authored 2 months ago
106
                    .padding(18)
107
                    .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
Bogdan Timofte authored 2 months ago
108

            
109
                    if usbMeter.supportsDataGroupCommands || usbMeter.recordedAH > 0 || usbMeter.recordedWH > 0 || usbMeter.recordingDuration > 0 {
110
                        VStack(alignment: .leading, spacing: 12) {
111
                            Text("Meter Totals")
Bogdan Timofte authored 2 months ago
112
                                .font(.headline)
Bogdan Timofte authored 2 months ago
113
                            ChargeRecordMetricsTableView(
Bogdan Timofte authored 2 months ago
114
                                labels: ["Capacity", "Energy", "Duration", "Meter Threshold"],
115
                                values: [
116
                                    "\(usbMeter.recordedAH.format(decimalDigits: 3)) Ah",
117
                                    "\(usbMeter.recordedWH.format(decimalDigits: 3)) Wh",
118
                                    usbMeter.recordingDurationDescription,
119
                                    usbMeter.supportsRecordingThreshold ? "\(usbMeter.recordingTreshold.format(decimalDigits: 2)) A" : "Read-only"
120
                                ]
121
                            )
Bogdan Timofte authored 2 months ago
122
                            Text("These values are reported by the meter for the active data group.")
123
                                .font(.footnote)
124
                                .foregroundColor(.secondary)
125
                            if usbMeter.supportsDataGroupCommands {
126
                                Button("Reset Active Group") {
127
                                    usbMeter.clear()
128
                                }
129
                                .frame(maxWidth: .infinity)
Bogdan Timofte authored 2 months ago
130
                                .padding(.vertical, 10)
131
                                .meterCard(tint: .red, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
132
                                .buttonStyle(.plain)
Bogdan Timofte authored 2 months ago
133
                            }
134
                        }
Bogdan Timofte authored 2 months ago
135
                        .padding(18)
136
                        .meterCard(tint: .teal, fillOpacity: 0.14, strokeOpacity: 0.20)
Bogdan Timofte authored 2 months ago
137
                    }
Bogdan Timofte authored 2 months ago
138
                }
139
                .padding()
Bogdan Timofte authored 2 months ago
140
            }
Bogdan Timofte authored 2 months ago
141
            .background(
142
                LinearGradient(
143
                    colors: [.pink.opacity(0.14), Color.clear],
144
                    startPoint: .topLeading,
145
                    endPoint: .bottomTrailing
146
                )
147
                .ignoresSafeArea()
148
            )
Bogdan Timofte authored 2 months ago
149
            .navigationBarTitle("Charge Record", displayMode: .inline)
Bogdan Timofte authored 2 months ago
150
            .navigationBarItems(trailing: Button("Done") { visibility.toggle() })
Bogdan Timofte authored 2 months ago
151
        }
Bogdan Timofte authored 2 months ago
152
        .navigationViewStyle(StackNavigationViewStyle())
Bogdan Timofte authored a month ago
153
        .sheet(isPresented: $chargedDeviceLibraryVisibility) {
154
            ChargedDeviceLibrarySheetView(
155
                visibility: $chargedDeviceLibraryVisibility,
156
                meterMACAddress: usbMeter.btSerial.macAddress.description,
157
                meterTint: usbMeter.color,
158
                mode: .device
159
            )
160
            .environmentObject(appData)
161
        }
162
        .sheet(isPresented: $chargerLibraryVisibility) {
163
            ChargedDeviceLibrarySheetView(
164
                visibility: $chargerLibraryVisibility,
165
                meterMACAddress: usbMeter.btSerial.macAddress.description,
166
                meterTint: usbMeter.color,
167
                mode: .charger
168
            )
169
            .environmentObject(appData)
170
        }
171
        .sheet(isPresented: $checkpointEditorVisibility) {
172
            BatteryCheckpointEditorSheetView()
173
                .environmentObject(appData)
174
                .environmentObject(usbMeter)
175
        }
176
        .sheet(item: $editingChargedDevice) { chargedDevice in
177
            ChargedDeviceEditorSheetView(
178
                meterMACAddress: nil,
179
                chargedDevice: chargedDevice
180
            )
181
            .environmentObject(appData)
182
        }
183
        .sheet(isPresented: $targetNotificationEditorVisibility) {
184
            if let activeChargeSession {
185
                BatteryTargetNotificationEditorSheetView(
186
                    sessionID: activeChargeSession.id,
187
                    initialTargetPercent: activeChargeSession.targetBatteryPercent
188
                )
189
                .environmentObject(appData)
190
            }
191
        }
192
    }
193

            
194
    private var selectedChargedDevice: ChargedDeviceSummary? {
195
        appData.currentChargedDeviceSummary(for: usbMeter.btSerial.macAddress.description)
196
    }
197

            
198
    private var activeChargeSession: ChargeSessionSummary? {
199
        appData.activeChargeSessionSummary(for: usbMeter.btSerial.macAddress.description)
200
    }
201

            
202
    private var selectedCharger: ChargedDeviceSummary? {
203
        appData.currentChargerSummary(for: usbMeter.btSerial.macAddress.description)
204
    }
205

            
206
    private var chargedDeviceSection: some View {
207
        VStack(alignment: .leading, spacing: 12) {
208
            HStack {
209
                Text("Device")
210
                    .font(.headline)
211
                Spacer()
212
                Button("Library") {
213
                    chargedDeviceLibraryVisibility = true
214
                }
215
            }
216

            
217
            if let selectedChargedDevice {
218
                HStack(alignment: .top, spacing: 14) {
219
                    ChargedDeviceQRCodeView(
220
                        qrIdentifier: selectedChargedDevice.qrIdentifier,
221
                        side: 88
222
                    )
223

            
224
                    VStack(alignment: .leading, spacing: 8) {
225
                        Label(selectedChargedDevice.name, systemImage: selectedChargedDevice.deviceClass.symbolName)
226
                            .font(.headline)
227

            
228
                        Text(selectedChargedDevice.deviceClass.title)
229
                            .font(.caption.weight(.semibold))
230
                            .foregroundColor(.secondary)
231

            
232
                        Text(selectedChargedDevice.supportsChargingWhileOff ? "Can finish charging while off" : "Needs on-state sessions to estimate capacity carefully")
233
                            .font(.caption2)
234
                            .foregroundColor(.secondary)
235

            
236
                        if selectedChargedDevice.supportedChargingModes.count == 1 {
237
                            Label(
238
                                "Charging via \(selectedChargedDevice.preferredChargingTransportMode.title)",
239
                                systemImage: selectedChargedDevice.preferredChargingTransportMode.symbolName
240
                            )
241
                            .font(.caption2)
242
                            .foregroundColor(.secondary)
243
                        } else {
244
                            Picker("Charging Type", selection: chargingTransportModeBinding(for: selectedChargedDevice)) {
245
                                ForEach(selectedChargedDevice.supportedChargingModes) { chargingTransportMode in
246
                                    Label(chargingTransportMode.title, systemImage: chargingTransportMode.symbolName)
247
                                        .tag(chargingTransportMode)
248
                                }
249
                            }
250
                            .pickerStyle(.segmented)
251
                        }
252

            
253
                        if let capacity = selectedChargedDevice.estimatedBatteryCapacityWh(for: effectiveChargingTransportMode(for: selectedChargedDevice)) {
254
                            Text("Estimated \(effectiveChargingTransportMode(for: selectedChargedDevice).title.lowercased()) capacity: \(capacity.format(decimalDigits: 2)) Wh")
255
                                .font(.caption)
256
                                .foregroundColor(.secondary)
257
                        } else if let capacity = selectedChargedDevice.estimatedBatteryCapacityWh {
258
                            Text("Estimated capacity: \(capacity.format(decimalDigits: 2)) Wh")
259
                                .font(.caption)
260
                                .foregroundColor(.secondary)
261
                        }
262

            
263
                        if let minimumCurrent = selectedChargedDevice.resolvedCompletionCurrentAmps(for: effectiveChargingTransportMode(for: selectedChargedDevice)) {
264
                            Text("\(effectiveChargingTransportMode(for: selectedChargedDevice).title) completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
265
                                .font(.caption2)
266
                                .foregroundColor(.secondary)
267
                        } else if let minimumCurrent = selectedChargedDevice.minimumCurrentAmps {
268
                            Text("Completion current: \(minimumCurrent.format(decimalDigits: 2)) A")
269
                                .font(.caption2)
270
                                .foregroundColor(.secondary)
271
                        }
272
                    }
273

            
274
                    Spacer(minLength: 0)
275
                }
276

            
277
                if shouldShowWirelessChargerSection(for: selectedChargedDevice) {
278
                    Divider()
279

            
280
                    VStack(alignment: .leading, spacing: 10) {
281
                        HStack {
282
                            Text("Wireless Charger")
283
                                .font(.subheadline.weight(.semibold))
284
                            Spacer()
285
                            Button(selectedCharger == nil ? "Select" : "Change") {
286
                                chargerLibraryVisibility = true
287
                            }
288
                        }
289

            
290
                        if let selectedCharger {
291
                            HStack(alignment: .top, spacing: 12) {
292
                                ChargedDeviceQRCodeView(
293
                                    qrIdentifier: selectedCharger.qrIdentifier,
294
                                    side: 62
295
                                )
296

            
297
                                VStack(alignment: .leading, spacing: 6) {
298
                                    Label(selectedCharger.name, systemImage: selectedCharger.deviceClass.symbolName)
299
                                        .font(.subheadline.weight(.semibold))
300

            
301
                                    if let chargerMaximumPowerWatts = selectedCharger.chargerMaximumPowerWatts {
302
                                        Text("Max power: \(chargerMaximumPowerWatts.format(decimalDigits: 2)) W")
303
                                            .font(.caption)
304
                                            .foregroundColor(.secondary)
305
                                    }
306

            
307
                                    if !selectedCharger.chargerObservedVoltageSelections.isEmpty {
308
                                        Text(
309
                                            "Observed voltages: " + selectedCharger.chargerObservedVoltageSelections
310
                                                .map { "\($0.format(decimalDigits: 1)) V" }
311
                                                .joined(separator: ", ")
312
                                        )
313
                                        .font(.caption2)
314
                                        .foregroundColor(.secondary)
315
                                    }
316
                                }
317
                            }
318
                        } else {
319
                            Text("Wireless sessions need a selected charger in addition to the charged device.")
320
                                .font(.caption)
321
                                .foregroundColor(.secondary)
322
                        }
323
                    }
324
                }
325

            
326
                Button("Add Battery Checkpoint") {
327
                    checkpointEditorVisibility = true
328
                }
329
                .frame(maxWidth: .infinity)
330
                .padding(.vertical, 10)
331
                .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
332
                .buttonStyle(.plain)
333

            
334
                Button("Edit Device") {
335
                    editingChargedDevice = selectedChargedDevice
336
                }
337
                .frame(maxWidth: .infinity)
338
                .padding(.vertical, 10)
339
                .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
340
                .buttonStyle(.plain)
341
            } else {
342
                Text("Select or create the device you are charging. New sessions, checkpoints, QR identity, capacity tracking, and curve learning are all anchored to that device.")
343
                    .font(.footnote)
344
                    .foregroundColor(.secondary)
345
            }
346
        }
347
        .padding(18)
348
        .meterCard(tint: .orange, fillOpacity: 0.14, strokeOpacity: 0.20)
349
    }
350

            
351
    private func chargeMonitorSection(_ activeChargeSession: ChargeSessionSummary) -> some View {
352
        VStack(alignment: .leading, spacing: 12) {
353
            Text("Charging Monitor")
354
                .font(.headline)
355

            
356
            ChargeRecordMetricsTableView(
357
                labels: ["Source", "Energy", "Charge", "Stop Threshold"],
358
                values: [
359
                    activeChargeSession.sourceMode.title,
360
                    "\(activeChargeSession.measuredEnergyWh.format(decimalDigits: 3)) Wh",
361
                    "\(activeChargeSession.measuredChargeAh.format(decimalDigits: 3)) Ah",
362
                    "\(activeChargeSession.stopThresholdAmps.format(decimalDigits: 2)) A"
363
                ]
364
            )
365

            
366
            if let selectedChargedDevice,
367
               let batteryPrediction = selectedChargedDevice.batteryLevelPrediction(for: activeChargeSession) {
368
                VStack(alignment: .leading, spacing: 4) {
369
                    Text("Predicted battery level: \(batteryPrediction.predictedPercent.format(decimalDigits: 0))%")
370
                        .font(.caption.weight(.semibold))
371
                    Text(
372
                        "Anchored to \(batteryPrediction.anchorDescription) at \(batteryPrediction.anchorPercent.format(decimalDigits: 0))% using \(batteryPrediction.estimatedCapacityWh.format(decimalDigits: 2)) Wh estimated capacity."
373
                    )
374
                    .font(.caption2)
375
                    .foregroundColor(.secondary)
376
                }
377
            }
378

            
379
            if let targetBatteryPercent = activeChargeSession.targetBatteryPercent {
380
                Text("Target notification: \(targetBatteryPercent.format(decimalDigits: 0))%")
381
                    .font(.caption.weight(.semibold))
382
            } else {
383
                Text("No target battery notification configured.")
384
                    .font(.caption)
385
                    .foregroundColor(.secondary)
386
            }
387

            
388
            Button(activeChargeSession.targetBatteryPercent == nil ? "Set Target Notification" : "Change Target Notification") {
389
                targetNotificationEditorVisibility = true
390
            }
391
            .frame(maxWidth: .infinity)
392
            .padding(.vertical, 10)
393
            .meterCard(tint: .indigo, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
394
            .buttonStyle(.plain)
395

            
396
            if activeChargeSession.targetBatteryPercent != nil {
397
                Button("Clear Target Notification") {
398
                    _ = appData.setTargetBatteryPercent(nil, for: activeChargeSession.id)
399
                }
400
                .frame(maxWidth: .infinity)
401
                .padding(.vertical, 10)
402
                .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
403
                .buttonStyle(.plain)
404
            }
405

            
406
            if activeChargeSession.requiresCompletionConfirmation {
407
                completionConfirmationCard(activeChargeSession)
408
            }
409

            
410
            if let capacityEstimateWh = activeChargeSession.capacityEstimateWh {
411
                Text("Current \(activeChargeSession.chargingTransportMode.title.lowercased()) capacity estimate: \(capacityEstimateWh.format(decimalDigits: 2)) Wh")
412
                    .font(.caption.weight(.semibold))
413
            }
414

            
415
            Label(
416
                "Session charging type: \(activeChargeSession.chargingTransportMode.title)",
417
                systemImage: activeChargeSession.chargingTransportMode.symbolName
418
            )
419
            .font(.caption)
420
            .foregroundColor(.secondary)
421

            
422
            if activeChargeSession.chargingTransportMode == .wireless {
423
                if let chargerID = activeChargeSession.chargerID,
424
                   let charger = appData.chargedDeviceSummary(id: chargerID) {
425
                    Label("Wireless charger: \(charger.name)", systemImage: "bolt.badge.clock")
426
                        .font(.caption)
427
                        .foregroundColor(.secondary)
428
                } else {
429
                    Text("No wireless charger is currently selected for this session.")
430
                        .font(.caption)
431
                        .foregroundColor(.orange)
432
                }
433
            }
434

            
435
            if activeChargeSession.checkpoints.isEmpty == false {
436
                VStack(alignment: .leading, spacing: 8) {
437
                    Text("Battery Checkpoints")
438
                        .font(.subheadline.weight(.semibold))
439

            
440
                    ForEach(activeChargeSession.checkpoints.suffix(4), id: \.id) { checkpoint in
441
                        HStack {
442
                            Text(checkpoint.timestamp.format())
443
                                .font(.caption2)
444
                                .foregroundColor(.secondary)
445
                            Spacer()
446
                            Text("\(checkpoint.batteryPercent.format(decimalDigits: 0))%")
447
                                .font(.caption.weight(.semibold))
448
                            Text("•")
449
                                .foregroundColor(.secondary)
450
                            Text("\(checkpoint.measuredEnergyWh.format(decimalDigits: 2)) Wh")
451
                                .font(.caption2)
452
                                .foregroundColor(.secondary)
453
                        }
454
                    }
455
                }
456
            }
457

            
458
            Text("The monitor prefers the meter's offline counters when available, then blends them with live samples so reconnects do not lose the real transferred energy.")
459
                .font(.footnote)
460
                .foregroundColor(.secondary)
461
        }
462
        .padding(18)
463
        .meterCard(tint: .green, fillOpacity: 0.14, strokeOpacity: 0.20)
464
    }
465

            
466
    private func completionConfirmationCard(_ activeChargeSession: ChargeSessionSummary) -> some View {
467
        VStack(alignment: .leading, spacing: 10) {
468
            Text("Completion Needs Confirmation")
469
                .font(.subheadline.weight(.semibold))
470

            
471
            if let contradictionPercent = activeChargeSession.completionContradictionPercent {
472
                Text("Current dropped below the stop threshold, but the estimated battery level is only \(contradictionPercent.format(decimalDigits: 0))%.")
473
                    .font(.caption)
474
                    .foregroundColor(.secondary)
475
            } else {
476
                Text("Current dropped below the stop threshold, but the estimated battery level does not match a normal charge end.")
477
                    .font(.caption)
478
                    .foregroundColor(.secondary)
479
            }
480

            
481
            if let targetBatteryPercent = activeChargeSession.targetBatteryPercent {
482
                Text("The active target is \(targetBatteryPercent.format(decimalDigits: 0))%.")
483
                    .font(.caption2)
484
                    .foregroundColor(.secondary)
485
            }
486

            
487
            Button("Finish Session") {
488
                _ = appData.confirmChargeSessionCompletion(sessionID: activeChargeSession.id)
489
            }
490
            .frame(maxWidth: .infinity)
491
            .padding(.vertical, 10)
492
            .meterCard(tint: .orange, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
493
            .buttonStyle(.plain)
494

            
495
            Button("Keep Monitoring") {
496
                _ = appData.continueChargeSessionMonitoring(sessionID: activeChargeSession.id)
497
            }
498
            .frame(maxWidth: .infinity)
499
            .padding(.vertical, 10)
500
            .meterCard(tint: .blue, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
501
            .buttonStyle(.plain)
502
        }
503
        .padding(14)
504
        .meterCard(tint: .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 16)
505
    }
506

            
507
    private func chargingTransportModeBinding(for chargedDevice: ChargedDeviceSummary) -> Binding<ChargingTransportMode> {
508
        Binding(
509
            get: {
510
                effectiveChargingTransportMode(for: chargedDevice)
511
            },
512
            set: { newValue in
513
                _ = appData.setChargingTransportMode(newValue, for: usbMeter)
514
            }
515
        )
516
    }
517

            
518
    private func effectiveChargingTransportMode(for chargedDevice: ChargedDeviceSummary) -> ChargingTransportMode {
519
        activeChargeSession?.chargingTransportMode ?? chargedDevice.preferredChargingTransportMode
520
    }
521

            
522
    private func shouldShowWirelessChargerSection(for chargedDevice: ChargedDeviceSummary) -> Bool {
523
        effectiveChargingTransportMode(for: chargedDevice) == .wireless
Bogdan Timofte authored 2 months ago
524
    }
525
}
526

            
Bogdan Timofte authored 2 months ago
527
struct ChargeRecordSheetView_Previews: PreviewProvider {
Bogdan Timofte authored 2 months ago
528
    static var previews: some View {
Bogdan Timofte authored 2 months ago
529
        ChargeRecordSheetView(visibility: .constant(true))
Bogdan Timofte authored 2 months ago
530
    }
531
}
Bogdan Timofte authored a month ago
532

            
Bogdan Timofte authored a month ago
533
struct BatteryTargetNotificationEditorSheetView: View {
Bogdan Timofte authored a month ago
534
    @Environment(\.dismiss) private var dismiss
535
    @EnvironmentObject private var appData: AppData
536

            
537
    let sessionID: UUID
538
    let initialTargetPercent: Double?
539

            
540
    @State private var targetPercent: Double
541

            
542
    init(sessionID: UUID, initialTargetPercent: Double?) {
543
        self.sessionID = sessionID
544
        self.initialTargetPercent = initialTargetPercent
545
        _targetPercent = State(initialValue: initialTargetPercent ?? 80)
546
    }
547

            
548
    var body: some View {
549
        NavigationView {
550
            Form {
551
                Section(header: Text("Target Level")) {
552
                    VStack(alignment: .leading, spacing: 12) {
553
                        Text("\(targetPercent.format(decimalDigits: 0))%")
554
                            .font(.title3.weight(.bold))
555
                        Slider(value: $targetPercent, in: 20...100, step: 1)
556
                        Text("A local notification will be generated on synced devices when the estimated battery level reaches this target.")
557
                            .font(.footnote)
558
                            .foregroundColor(.secondary)
559
                    }
560
                }
561
            }
562
            .navigationTitle("Battery Target")
563
            .navigationBarTitleDisplayMode(.inline)
564
            .toolbar {
565
                ToolbarItem(placement: .cancellationAction) {
566
                    Button("Cancel") {
567
                        dismiss()
568
                    }
569
                }
570

            
571
                ToolbarItem(placement: .confirmationAction) {
572
                    Button("Save") {
573
                        if appData.setTargetBatteryPercent(targetPercent, for: sessionID) {
574
                            dismiss()
575
                        }
576
                    }
577
                }
578
            }
579
        }
580
    }
581
}