USB-Meter / USB Meter / Views / ChargedDevices / BatteryCheckpointEditorSheetView.swift
1 contributor
175 lines | 6.249kb
//
//  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())
    }
}