// // BatteryCheckpointEditorSheetView.swift // USB Meter // // Created by Codex on 10/04/2026. // import SwiftUI struct BatteryCheckpointEditorContentView: View { @EnvironmentObject private var appData: AppData let sessionID: UUID let message: String let effectiveEnergyWhOverride: Double? let measuredChargeAhOverride: Double? let onCancel: (() -> Void)? let onSaved: (() -> Void)? @State private var batteryPercent = "" @State private var showsWarningPopover = false private var plausibilityWarning: BatteryCheckpointPlausibilityWarning? { guard let percent = normalizedBatteryPercent else { return nil } return appData.batteryCheckpointPlausibilityWarning( percent: percent, for: sessionID, effectiveEnergyWhOverride: effectiveEnergyWhOverride ) } private var normalizedBatteryPercent: Double? { let normalized = batteryPercent .trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: ",", with: ".") return Double(normalized) } private var canSave: Bool { guard let percent = normalizedBatteryPercent else { return false } return percent >= 0 && percent <= 100 } var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 8) { Text("Checkpoint") Spacer(minLength: 0) if let plausibilityWarning { Button { showsWarningPopover.toggle() } label: { Image(systemName: "exclamationmark.triangle.fill") .font(.body.weight(.semibold)) .foregroundColor(.orange) } .buttonStyle(.plain) .accessibilityLabel(plausibilityWarning.title) .popover(isPresented: $showsWarningPopover, arrowEdge: .top) { VStack(alignment: .leading, spacing: 10) { Text(plausibilityWarning.title) .font(.headline) Text(plausibilityWarning.message) .font(.body) .fixedSize(horizontal: false, vertical: true) } .padding(16) .frame(width: 320, alignment: .leading) } } ContextInfoButton( title: "Checkpoint", message: message ) } VStack(alignment: .leading, spacing: 10) { TextField("Battery %", text: $batteryPercent) .keyboardType(.decimalPad) .textFieldStyle(.roundedBorder) } HStack(spacing: 10) { if let onCancel { Button("Cancel") { onCancel() } .frame(maxWidth: .infinity) .padding(.vertical, 10) .meterCard(tint: .secondary, fillOpacity: 0.12, strokeOpacity: 0.18, cornerRadius: 14) .buttonStyle(.plain) } Button("Save Checkpoint") { saveCheckpoint() } .frame(maxWidth: .infinity) .padding(.vertical, 10) .meterCard(tint: .green, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14) .buttonStyle(.plain) .disabled(!canSave) .opacity(canSave ? 1 : 0.6) } } } private func saveCheckpoint() { guard let percent = normalizedBatteryPercent else { return } if appData.addBatteryCheckpoint( percent: percent, for: sessionID, measuredEnergyWh: effectiveEnergyWhOverride, measuredChargeAh: measuredChargeAhOverride ) { onSaved?() } } } struct BatteryCheckpointEditorSheetView: View { @EnvironmentObject private var appData: AppData @EnvironmentObject private var meter: Meter @Environment(\.dismiss) private var dismiss private var activeSession: ChargeSessionSummary? { appData.activeChargeSessionSummary(for: meter.btSerial.macAddress.description) } var body: some View { NavigationView { Group { if let activeSession { Form { BatteryCheckpointEditorContentView( sessionID: activeSession.id, message: "The checkpoint is stored on the active charge session and later used for capacity estimation and the typical charge curve.", effectiveEnergyWhOverride: nil, measuredChargeAhOverride: nil, onCancel: { dismiss() }, onSaved: { dismiss() } ) } } else { VStack(spacing: 12) { Image(systemName: "bolt.slash") .font(.title2) .foregroundColor(.secondary) Text("No Active Session") .font(.headline) Text("Start a charging session before adding a battery checkpoint.") .font(.footnote) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .padding(24) } } .navigationTitle("Battery Checkpoint") .navigationBarTitleDisplayMode(.inline) } .navigationViewStyle(StackNavigationViewStyle()) } }