Files
Cable/Cable/Chargers/ChargersView.swift
Stefan Lange-Hegermann 23b117bfe2 Polish editors, previews, persistence and docs
Cross-platform refinements to appearance/battery/charger editors, tabs
and navigation, plus persistence, screenshot previews and CLAUDE.md docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 01:04:47 +02:00

402 lines
12 KiB
Swift

import SwiftUI
struct ChargersView: View {
@Binding var editMode: EditMode
let system: ElectricalSystem
let chargers: [SavedCharger]
let onAdd: () -> Void
let onEdit: (SavedCharger) -> Void
let onDelete: (IndexSet) -> Void
let onBrowseLibrary: () -> Void
private struct SummaryMetric: Identifiable {
let id: String
let icon: String
let label: String
let value: String
let tint: Color
}
private var summaryTitle: String {
String(
localized: "chargers.summary.title",
bundle: .main,
comment: "Title for the chargers summary section"
)
}
private var summaryCountLabel: String {
String(
localized: "chargers.summary.metric.count",
bundle: .main,
comment: "Label for number of chargers metric"
)
}
private var summaryOutputLabel: String {
String(
localized: "chargers.summary.metric.output",
bundle: .main,
comment: "Label for output voltage metric"
)
}
private var summaryCurrentLabel: String {
String(
localized: "chargers.summary.metric.current",
bundle: .main,
comment: "Label for combined current metric"
)
}
private var summaryPowerLabel: String {
String(
localized: "chargers.summary.metric.power",
bundle: .main,
comment: "Label for combined power metric"
)
}
private var badgeInputLabel: String {
String(
localized: "chargers.badge.input",
bundle: .main,
comment: "Label for input voltage badge"
)
}
private var badgeOutputLabel: String {
String(
localized: "chargers.badge.output",
bundle: .main,
comment: "Label for output voltage badge"
)
}
private var badgeCurrentLabel: String {
String(
localized: "chargers.badge.current",
bundle: .main,
comment: "Label for charging current badge"
)
}
private var badgePowerLabel: String {
String(
localized: "chargers.badge.power",
bundle: .main,
comment: "Label for charging power badge"
)
}
init(
system: ElectricalSystem,
chargers: [SavedCharger],
editMode: Binding<EditMode> = .constant(.inactive),
onAdd: @escaping () -> Void = {},
onEdit: @escaping (SavedCharger) -> Void = { _ in },
onDelete: @escaping (IndexSet) -> Void = { _ in },
onBrowseLibrary: @escaping () -> Void = {}
) {
self.system = system
self.chargers = chargers
self.onAdd = onAdd
self.onEdit = onEdit
self.onDelete = onDelete
self.onBrowseLibrary = onBrowseLibrary
_editMode = editMode
}
var body: some View {
VStack(spacing: 0) {
if chargers.isEmpty {
emptyState
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
chargersListWithHeader
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
}
private var chargerStatsHeader: some View {
StatsHeaderContainer {
chargerSummaryContent
}
}
private var chargerSummaryContent: some View {
VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
Text(summaryTitle)
.font(.headline.weight(.semibold))
Spacer()
}
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(summaryMetrics) { metric in
ComponentSummaryMetricView(
icon: metric.icon,
label: metric.label,
value: metric.value,
tint: metric.tint
)
}
}
.padding(.horizontal, 2)
}
.scrollClipDisabled(false)
}
}
}
@ViewBuilder
private var chargersListWithHeader: some View {
if #available(iOS 26.0, *) {
baseChargersList
.scrollEdgeEffectStyle(.soft, for: .top)
.safeAreaInset(edge: .top, spacing: 0) {
chargerStatsHeader
}
} else {
baseChargersList
.safeAreaInset(edge: .top, spacing: 0) {
chargerStatsHeader
}
}
}
private var baseChargersList: some View {
List {
ForEach(chargers) { charger in
Button {
onEdit(charger)
} label: {
chargerRow(for: charger)
}
.buttonStyle(.plain)
.disabled(editMode == .active)
.listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16))
.listRowSeparator(.hidden)
.listRowBackground(Color.clear)
}
.onDelete(perform: onDelete)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.environment(\.editMode, $editMode)
.accessibilityIdentifier("chargers-list")
}
private var summaryMetrics: [SummaryMetric] {
guard !chargers.isEmpty else { return [] }
var metrics: [SummaryMetric] = [
SummaryMetric(
id: "count",
icon: "bolt.fill",
label: summaryCountLabel,
value: "\(chargers.count)",
tint: .blue
)
]
if let output = representativeOutputVoltage {
metrics.append(
SummaryMetric(
id: "output",
icon: "battery.100.bolt",
label: summaryOutputLabel,
value: formattedVoltage(output),
tint: .green
)
)
}
if totalCurrent > 0 {
metrics.append(
SummaryMetric(
id: "current",
icon: "gauge",
label: summaryCurrentLabel,
value: formattedCurrent(totalCurrent),
tint: .orange
)
)
}
if totalPower > 0 {
metrics.append(
SummaryMetric(
id: "power",
icon: "bolt.badge.a",
label: summaryPowerLabel,
value: formattedPower(totalPower),
tint: .pink
)
)
}
return metrics
}
private var emptyState: some View {
OnboardingInfoView(
configuration: .charger(),
onPrimaryAction: onAdd,
onSecondaryAction: onBrowseLibrary
)
.padding(.horizontal, 0)
}
private func chargerRow(for charger: SavedCharger) -> some View {
VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 12) {
LoadIconView(
remoteIconURLString: charger.remoteIconURLString,
fallbackSystemName: charger.iconName,
fallbackColor: Color.componentColor(named: charger.colorName),
size: 48
)
VStack(alignment: .leading, spacing: 4) {
Text(charger.name)
.font(.body.weight(.medium))
.lineLimit(1)
.truncationMode(.tail)
Text(chargerSummary(for: charger))
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
Capsule(style: .continuous)
.fill(Color(.tertiarySystemBackground))
)
}
Spacer()
if editMode == .inactive {
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.secondary)
}
}
metricsSection(for: charger)
}
.padding(.vertical, 16)
.padding(.horizontal, 16)
.background(
RoundedRectangle(cornerRadius: 18, style: .continuous)
.fill(Color(.systemBackground))
)
}
@ViewBuilder
private func metricsSection(for charger: SavedCharger) -> some View {
let badges: [(String, String, Color)] = [
(badgeInputLabel, formattedVoltage(charger.inputVoltage), .indigo),
(badgeOutputLabel, formattedVoltage(charger.outputVoltage), .green),
(badgeCurrentLabel, formattedCurrent(charger.maxCurrentAmps), .orange),
(badgePowerLabel, formattedPower(charger.effectivePowerWatts), .pink)
]
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(badges, id: \.0) { label, value, tint in
ComponentMetricBadgeView(label: label, value: value, tint: tint)
}
}
.padding(.trailing, 16)
}
.scrollClipDisabled(true)
}
private func chargerSummary(for charger: SavedCharger) -> String {
let inputText = formattedVoltage(charger.inputVoltage)
let outputText = formattedVoltage(charger.outputVoltage)
let currentText = formattedCurrent(charger.maxCurrentAmps)
return [inputText, outputText, currentText].joined(separator: " • ")
}
private var totalCurrent: Double {
chargers.reduce(0) { result, charger in
result + max(0, charger.maxCurrentAmps)
}
}
private var totalPower: Double {
chargers.reduce(0) { result, charger in
result + max(0, charger.effectivePowerWatts)
}
}
private var representativeOutputVoltage: Double? {
let outputs = chargers.map { $0.outputVoltage }.filter { $0 > 0 }
guard !outputs.isEmpty else { return nil }
let total = outputs.reduce(0, +)
return total / Double(outputs.count)
}
private func formattedVoltage(_ value: Double) -> String {
guard value > 0 else { return "—" }
return String(format: "%.1fV", value)
}
private func formattedCurrent(_ value: Double) -> String {
guard value > 0 else { return "—" }
return String(format: "%.1fA", value)
}
private func formattedPower(_ value: Double) -> String {
guard value > 0 else { return "—" }
return String(format: "%.0fW", value)
}
}
private enum ChargersViewPreviewData {
static let system = ElectricalSystem(name: "Preview System", iconName: "bolt", colorName: "teal")
static let chargers: [SavedCharger] = {
let shore = SavedCharger(
name: "Shore Power",
inputVoltage: 230,
outputVoltage: 14.4,
maxCurrentAmps: 40,
maxPowerWatts: 580,
iconName: "powerplug",
colorName: "orange",
system: system
)
shore.timestamp = Date(timeIntervalSinceReferenceDate: 2000)
let dcDc = SavedCharger(
name: "DC-DC Charger",
inputVoltage: 12.8,
outputVoltage: 14.2,
maxCurrentAmps: 30,
maxPowerWatts: 0,
iconName: "bolt.badge.clock",
colorName: "blue",
system: system
)
dcDc.timestamp = Date(timeIntervalSinceReferenceDate: 2100)
return [shore, dcDc]
}()
}
#Preview {
ChargersView(
system: ChargersViewPreviewData.system,
chargers: ChargersViewPreviewData.chargers,
editMode: .constant(.inactive)
)
}