1 contributor
//
// ChargeSessionCompletionSheetView.swift
// USB Meter
//
import SwiftUI
struct ChargeSessionCompletionSheetView: View {
private enum FinalCheckpoint: String, CaseIterable, Identifiable {
case full
case skip
case custom
var id: String { rawValue }
var label: String {
switch self {
case .full: return "Full"
case .skip: return "Skip"
case .custom: return "Other %"
}
}
}
@EnvironmentObject private var appData: AppData
@Environment(\.dismiss) private var dismiss
let sessionID: UUID
let title: String
let confirmTitle: String
let explanation: String
@State private var batteryPercent = ""
@State private var finalCheckpoint: FinalCheckpoint = .skip
var body: some View {
NavigationView {
Form {
Section(
header: ContextInfoHeader(
title: "Final Checkpoint",
message: explanation
)
) {
Picker("Final Battery", selection: $finalCheckpoint) {
ForEach(FinalCheckpoint.allCases) { mode in
Text(mode.label).tag(mode)
}
}
.pickerStyle(.segmented)
if finalCheckpoint == .custom {
TextField("Battery %", text: $batteryPercent)
.keyboardType(.decimalPad)
}
}
Section {
if let refusalReason {
Label(refusalReason, systemImage: "exclamationmark.triangle.fill")
.font(.footnote)
.foregroundColor(.red)
Button(role: .destructive) {
_ = appData.deleteChargeSession(sessionID: sessionID)
dismiss()
} label: {
Label("Discard Session", systemImage: "trash")
}
} else if let customCheckpointWarning {
Label(customCheckpointWarning, systemImage: "exclamationmark.triangle.fill")
.font(.footnote)
.foregroundColor(.orange)
} else if let sessionWarning {
Text(sessionWarning)
.font(.footnote)
.foregroundColor(.orange)
} else if resolvedFinalBatteryPercent == 100 {
Text("A final checkpoint at 100% lets the app learn the stop current for this exact charging type when the session data is reliable.")
.font(.footnote)
.foregroundColor(.secondary)
}
}
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button(confirmTitle) {
guard canSave else { return }
if appData.stopChargeSession(sessionID: sessionID, finalBatteryPercent: resolvedFinalBatteryPercent) {
dismiss()
}
}
.disabled(!canSave)
.opacity(canSave ? 1 : 0.45)
}
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
private var session: ChargeSessionSummary? {
appData.chargedDevices
.flatMap(\.sessions)
.first(where: { $0.id == sessionID })
}
private var canSave: Bool {
session?.hasSavableChargeData == true
}
private var refusalReason: String? {
canSave ? nil : "This session has no charging data to save. Discard it instead."
}
private var customCheckpointWarning: String? {
guard finalCheckpoint == .custom,
batteryPercent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false,
parsedBatteryPercent == nil else {
return nil
}
return "Final battery percentage must be between 0 and 100. Save will close the session without a final checkpoint."
}
private var parsedBatteryPercent: Double? {
let normalized = batteryPercent
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: ",", with: ".")
guard let value = Double(normalized), value >= 0, value <= 100 else { return nil }
return value
}
private var resolvedFinalBatteryPercent: Double? {
switch finalCheckpoint {
case .full: return 100
case .skip: return nil
case .custom: return parsedBatteryPercent
}
}
private var sessionWarning: String? {
guard let session,
session.chargingTransportMode == .wireless,
let chargerID = session.chargerID,
let charger = appData.chargedDeviceSummary(id: chargerID),
charger.chargerIdleCurrentAmps == nil else {
return nil
}
return "This charger has no idle-current measurement, so the final checkpoint will stop the session but will not learn a wireless stop threshold yet."
}
}