1 contributor
//
// 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 label = ""
@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)
TextField("Label (optional)", text: $label)
.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,
label: label,
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())
}
}