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

            
8
import SwiftUI
9

            
Bogdan Timofte authored a month ago
10
struct BatteryCheckpointEditorContentView: View {
Bogdan Timofte authored a month ago
11
    @EnvironmentObject private var appData: AppData
Bogdan Timofte authored a month ago
12

            
13
    let sessionID: UUID
14
    let message: String
15
    let effectiveEnergyWhOverride: Double?
16
    let measuredChargeAhOverride: Double?
17
    let onCancel: (() -> Void)?
18
    let onSaved: (() -> Void)?
Bogdan Timofte authored a month ago
19

            
20
    @State private var batteryPercent = ""
Bogdan Timofte authored a month ago
21
    @State private var showsWarningPopover = false
Bogdan Timofte authored a month ago
22

            
23
    private var plausibilityWarning: BatteryCheckpointPlausibilityWarning? {
Bogdan Timofte authored a month ago
24
        guard let percent = normalizedBatteryPercent else {
Bogdan Timofte authored a month ago
25
            return nil
26
        }
Bogdan Timofte authored a month ago
27
        return appData.batteryCheckpointPlausibilityWarning(
28
            percent: percent,
29
            for: sessionID,
30
            effectiveEnergyWhOverride: effectiveEnergyWhOverride
31
        )
Bogdan Timofte authored a month ago
32
    }
Bogdan Timofte authored a month ago
33

            
Bogdan Timofte authored a month ago
34
    private var normalizedBatteryPercent: Double? {
35
        let normalized = batteryPercent
36
            .trimmingCharacters(in: .whitespacesAndNewlines)
37
            .replacingOccurrences(of: ",", with: ".")
38
        return Double(normalized)
39
    }
Bogdan Timofte authored a month ago
40

            
Bogdan Timofte authored a month ago
41
    private var canSave: Bool {
42
        guard let percent = normalizedBatteryPercent else {
43
            return false
44
        }
45
        return percent >= 0 && percent <= 100
46
    }
47

            
48
    var body: some View {
49
        VStack(alignment: .leading, spacing: 12) {
50
            HStack(spacing: 8) {
51
                Text("Checkpoint")
52
                Spacer(minLength: 0)
Bogdan Timofte authored a month ago
53
                if let plausibilityWarning {
Bogdan Timofte authored a month ago
54
                    Button {
55
                        showsWarningPopover.toggle()
56
                    } label: {
57
                        Image(systemName: "exclamationmark.triangle.fill")
58
                            .font(.body.weight(.semibold))
Bogdan Timofte authored a month ago
59
                            .foregroundColor(.orange)
60
                    }
Bogdan Timofte authored a month ago
61
                    .buttonStyle(.plain)
62
                    .accessibilityLabel(plausibilityWarning.title)
63
                    .popover(isPresented: $showsWarningPopover, arrowEdge: .top) {
64
                        VStack(alignment: .leading, spacing: 10) {
65
                            Text(plausibilityWarning.title)
66
                                .font(.headline)
67
                            Text(plausibilityWarning.message)
68
                                .font(.body)
69
                                .fixedSize(horizontal: false, vertical: true)
70
                        }
71
                        .padding(16)
72
                        .frame(width: 320, alignment: .leading)
73
                    }
Bogdan Timofte authored a month ago
74
                }
Bogdan Timofte authored a month ago
75
                ContextInfoButton(
76
                    title: "Checkpoint",
77
                    message: message
78
                )
Bogdan Timofte authored a month ago
79
            }
Bogdan Timofte authored a month ago
80

            
81
            VStack(alignment: .leading, spacing: 10) {
82
                TextField("Battery %", text: $batteryPercent)
83
                    .keyboardType(.decimalPad)
84
                    .textFieldStyle(.roundedBorder)
85
            }
86

            
87
            HStack(spacing: 10) {
88
                if let onCancel {
Bogdan Timofte authored a month ago
89
                    Button("Cancel") {
Bogdan Timofte authored a month ago
90
                        onCancel()
Bogdan Timofte authored a month ago
91
                    }
Bogdan Timofte authored a month ago
92
                    .frame(maxWidth: .infinity)
93
                    .padding(.vertical, 10)
94
                    .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14)
95
                    .buttonStyle(.plain)
Bogdan Timofte authored a month ago
96
                }
Bogdan Timofte authored a month ago
97

            
98
                Button("Save Checkpoint") {
99
                    saveCheckpoint()
Bogdan Timofte authored a month ago
100
                }
Bogdan Timofte authored a month ago
101
                .frame(maxWidth: .infinity)
102
                .padding(.vertical, 10)
103
                .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14)
104
                .buttonStyle(.plain)
105
                .disabled(!canSave)
106
                .opacity(canSave ? 1 : 0.6)
Bogdan Timofte authored a month ago
107
            }
108
        }
Bogdan Timofte authored a month ago
109
    }
110

            
Bogdan Timofte authored a month ago
111
    private func saveCheckpoint() {
112
        guard let percent = normalizedBatteryPercent else {
Bogdan Timofte authored a month ago
113
            return
114
        }
115

            
Bogdan Timofte authored a month ago
116
        if appData.addBatteryCheckpoint(
Bogdan Timofte authored a month ago
117
            percent: percent,
Bogdan Timofte authored a month ago
118
            for: sessionID,
119
            measuredEnergyWh: effectiveEnergyWhOverride,
120
            measuredChargeAh: measuredChargeAhOverride
121
        ) {
122
            onSaved?()
123
        }
124
    }
125
}
126

            
127
struct BatteryCheckpointEditorSheetView: View {
128
    @EnvironmentObject private var appData: AppData
129
    @EnvironmentObject private var meter: Meter
130
    @Environment(\.dismiss) private var dismiss
131

            
132
    private var activeSession: ChargeSessionSummary? {
133
        appData.activeChargeSessionSummary(for: meter.btSerial.macAddress.description)
134
    }
135

            
136
    var body: some View {
137
        NavigationView {
138
            Group {
139
                if let activeSession {
140
                    Form {
141
                        BatteryCheckpointEditorContentView(
142
                            sessionID: activeSession.id,
143
                            message: "The checkpoint is stored on the active charge session and later used for capacity estimation and the typical charge curve.",
144
                            effectiveEnergyWhOverride: nil,
145
                            measuredChargeAhOverride: nil,
146
                            onCancel: { dismiss() },
147
                            onSaved: { dismiss() }
148
                        )
149
                    }
150
                } else {
151
                    VStack(spacing: 12) {
152
                        Image(systemName: "bolt.slash")
153
                            .font(.title2)
154
                            .foregroundColor(.secondary)
155
                        Text("No Active Session")
156
                            .font(.headline)
157
                        Text("Start a charging session before adding a battery checkpoint.")
158
                            .font(.footnote)
159
                            .foregroundColor(.secondary)
160
                            .multilineTextAlignment(.center)
161
                    }
162
                    .padding(24)
163
                }
164
            }
165
            .navigationTitle("Battery Checkpoint")
166
            .navigationBarTitleDisplayMode(.inline)
Bogdan Timofte authored a month ago
167
        }
Bogdan Timofte authored a month ago
168
        .navigationViewStyle(StackNavigationViewStyle())
Bogdan Timofte authored a month ago
169
    }
170
}