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>
This commit is contained in:
2026-06-04 01:04:47 +02:00
parent d97e3a2b7c
commit 23b117bfe2
16 changed files with 287 additions and 89 deletions

View File

@@ -44,7 +44,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
var iconName: String
var colorName: String
var system: ElectricalSystem
var componentID: String?
init(
id: UUID = UUID(),
name: String,
@@ -58,7 +59,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
maximumTemperatureCelsius: Double = 60,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem
system: ElectricalSystem,
componentID: String? = nil
) {
self.id = id
self.name = name
@@ -73,6 +75,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.iconName = iconName
self.colorName = colorName
self.system = system
self.componentID = componentID
}
init(savedBattery: SavedBattery, system: ElectricalSystem) {
@@ -95,6 +98,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.iconName = savedBattery.iconName
self.colorName = savedBattery.colorName
self.system = system
self.componentID = savedBattery.componentID
}
var energyWattHours: Double {
@@ -137,6 +141,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
savedBattery.iconName = iconName
savedBattery.colorName = colorName
savedBattery.system = system
savedBattery.componentID = componentID
savedBattery.timestamp = Date()
}
}
@@ -154,7 +159,8 @@ extension BatteryConfiguration {
lhs.maximumTemperatureCelsius == rhs.maximumTemperatureCelsius &&
lhs.chemistry == rhs.chemistry &&
lhs.iconName == rhs.iconName &&
lhs.colorName == rhs.colorName
lhs.colorName == rhs.colorName &&
lhs.componentID == rhs.componentID
}
func hash(into hasher: inout Hasher) {
@@ -170,5 +176,6 @@ extension BatteryConfiguration {
hasher.combine(chemistry)
hasher.combine(iconName)
hasher.combine(colorName)
hasher.combine(componentID)
}
}

View File

@@ -12,6 +12,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
var colorName: String
var system: ElectricalSystem
var powerSourceType: SavedCharger.PowerSourceType
var componentID: String?
var remoteIconURLString: String?
init(
id: UUID = UUID(),
@@ -23,7 +25,9 @@ struct ChargerConfiguration: Identifiable, Hashable {
iconName: String = "bolt.fill",
colorName: String = "orange",
system: ElectricalSystem,
powerSourceType: SavedCharger.PowerSourceType = .shore
powerSourceType: SavedCharger.PowerSourceType = .shore,
componentID: String? = nil,
remoteIconURLString: String? = nil
) {
self.id = id
self.name = name
@@ -35,6 +39,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
self.colorName = colorName
self.system = system
self.powerSourceType = powerSourceType
self.componentID = componentID
self.remoteIconURLString = remoteIconURLString
}
init(savedCharger: SavedCharger, system: ElectricalSystem) {
@@ -48,6 +54,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
self.colorName = savedCharger.colorName
self.system = system
self.powerSourceType = savedCharger.sourceType
self.componentID = savedCharger.componentID
self.remoteIconURLString = savedCharger.remoteIconURLString
}
var effectivePowerWatts: Double {
@@ -67,6 +75,8 @@ struct ChargerConfiguration: Identifiable, Hashable {
savedCharger.colorName = colorName
savedCharger.system = system
savedCharger.powerSourceType = powerSourceType.rawValue
savedCharger.componentID = componentID
savedCharger.remoteIconURLString = remoteIconURLString
savedCharger.timestamp = Date()
}
}

View File

@@ -7,6 +7,7 @@ struct ChargersView: View {
let onAdd: () -> Void
let onEdit: (SavedCharger) -> Void
let onDelete: (IndexSet) -> Void
let onBrowseLibrary: () -> Void
private struct SummaryMetric: Identifiable {
let id: String
@@ -94,13 +95,15 @@ struct ChargersView: View {
editMode: Binding<EditMode> = .constant(.inactive),
onAdd: @escaping () -> Void = {},
onEdit: @escaping (SavedCharger) -> Void = { _ in },
onDelete: @escaping (IndexSet) -> 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
}
@@ -244,7 +247,8 @@ struct ChargersView: View {
private var emptyState: some View {
OnboardingInfoView(
configuration: .charger(),
onPrimaryAction: onAdd
onPrimaryAction: onAdd,
onSecondaryAction: onBrowseLibrary
)
.padding(.horizontal, 0)
}

View File

@@ -221,56 +221,56 @@ private struct ComponentLibraryScreenshot: View {
translations: ["de": "Navigationslichter", "es": "Luces de navegación", "fr": "Feux de navigation", "nl": "Navigatieverlichting"],
voltageIn: 12.8, voltageOut: nil, watt: 25,
dutyCyclePercent: 100, defaultUtilizationFactorPercent: 40,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "2", name: "Refrigerator Compressor",
translations: ["de": "Kühlschrank-Kompressor", "es": "Compresor de refrigerador", "fr": "Compresseur réfrigérateur", "nl": "Koelkastcompressor"],
voltageIn: 12.8, voltageOut: nil, watt: 48,
dutyCyclePercent: 40, defaultUtilizationFactorPercent: 100,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "3", name: "Anchor Windlass",
translations: ["de": "Ankerwinde", "es": "Molinete de ancla", "fr": "Guindeau", "nl": "Ankerlier"],
voltageIn: 12.8, voltageOut: nil, watt: 960,
dutyCyclePercent: 5, defaultUtilizationFactorPercent: 2,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "4", name: "VHF Radio",
translations: ["de": "UKW Funkgerät", "es": "Radio VHF", "fr": "Radio VHF", "nl": "Marifoon"],
voltageIn: 12.8, voltageOut: nil, watt: 72,
dutyCyclePercent: 30, defaultUtilizationFactorPercent: 33,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "5", name: "LED Interior Lights",
translations: ["de": "LED Innenbeleuchtung", "es": "Iluminación LED interior", "fr": "Éclairage LED intérieur", "nl": "LED binnenverlichting"],
voltageIn: 12.8, voltageOut: nil, watt: 18,
dutyCyclePercent: 100, defaultUtilizationFactorPercent: 25,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "6", name: "Water Pump",
translations: ["de": "Wasserpumpe", "es": "Bomba de agua", "fr": "Pompe à eau", "nl": "Waterpomp"],
voltageIn: 12.8, voltageOut: nil, watt: 42,
dutyCyclePercent: 20, defaultUtilizationFactorPercent: 10,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "7", name: "Diesel Heater",
translations: ["de": "Dieselheizung", "es": "Calefactor diésel", "fr": "Chauffage diesel", "nl": "Dieselverwarming"],
voltageIn: 12.8, voltageOut: nil, watt: 36,
dutyCyclePercent: 60, defaultUtilizationFactorPercent: 50,
iconURL: nil
componentCategory: nil, iconURL: nil
),
ComponentLibraryItem(
id: "8", name: "USB Charging Station",
translations: ["de": "USB-Ladestation", "es": "Estación de carga USB", "fr": "Station de charge USB", "nl": "USB-laadstation"],
voltageIn: 12.8, voltageOut: nil, watt: 24,
dutyCyclePercent: 100, defaultUtilizationFactorPercent: 30,
iconURL: nil
componentCategory: nil, iconURL: nil
),
]
}

View File

@@ -114,6 +114,87 @@ struct SystemComponentsPersistence {
)
}
static func makeBatteryDraft(
from item: ComponentLibraryItem,
for system: ElectricalSystem,
existingLoads: [SavedLoad],
existingBatteries: [SavedBattery],
existingChargers: [SavedCharger]
) -> BatteryConfiguration {
let localizedName = item.localizedName
let baseName = localizedName.isEmpty
? String(localized: "battery.editor.default_name", defaultValue: "New Battery")
: localizedName
let batteryName = uniqueName(
startingWith: baseName,
loads: existingLoads,
batteries: existingBatteries,
chargers: existingChargers
)
let nominalVoltage = item.displayVoltage ?? 12.8
let capacity = item.capacityAmpHours ?? 100
return BatteryConfiguration(
name: batteryName,
nominalVoltage: nominalVoltage,
capacityAmpHours: capacity,
chemistry: .lithiumIronPhosphate,
iconName: "battery.100",
colorName: system.colorName,
system: system,
componentID: item.id
)
}
static func makeChargerDraft(
from item: ComponentLibraryItem,
for system: ElectricalSystem,
existingLoads: [SavedLoad],
existingBatteries: [SavedBattery],
existingChargers: [SavedCharger]
) -> ChargerConfiguration {
let localizedName = item.localizedName
let baseName = localizedName.isEmpty
? String(localized: "charger.editor.default_name", defaultValue: "New Charger")
: localizedName
let chargerName = uniqueName(
startingWith: baseName,
loads: existingLoads,
batteries: existingBatteries,
chargers: existingChargers
)
let inputVoltage = item.voltageIn ?? LocaleDefaults.mainsVoltage
let outputVoltage = item.voltageOut ?? 14.2
let power = item.watt ?? 0
let current = item.outputCurrent ?? (outputVoltage > 0 ? power / outputVoltage : 30)
let sourceType = chargerSourceType(forCategory: item.componentCategory)
return ChargerConfiguration(
name: chargerName,
inputVoltage: inputVoltage,
outputVoltage: outputVoltage,
maxCurrentAmps: current,
maxPowerWatts: power,
iconName: sourceType.iconName,
colorName: system.colorName,
system: system,
powerSourceType: sourceType,
componentID: item.id,
remoteIconURLString: item.iconURL?.absoluteString
)
}
/// Maps a PocketBase `component_category` to a charger power source.
static func chargerSourceType(forCategory category: String?) -> SavedCharger.PowerSourceType {
guard let category = category?.lowercased(), !category.isEmpty else { return .shore }
if category.contains("solar") { return .solar }
if category.contains("wind") { return .wind }
if category.contains("dcdc") || category.contains("alternator") { return .alternator }
if category.contains("generator") { return .generator }
if category.contains("mains") || category.contains("shore") { return .shore }
return .shore
}
static func makeChargerDraft(
for system: ElectricalSystem,
existingLoads: [SavedLoad],
@@ -160,7 +241,8 @@ struct SystemComponentsPersistence {
maximumTemperatureCelsius: configuration.maximumTemperatureCelsius,
iconName: configuration.iconName,
colorName: configuration.colorName,
system: system
system: system,
componentID: configuration.componentID
)
context.insert(newBattery)
}
@@ -184,7 +266,9 @@ struct SystemComponentsPersistence {
maxPowerWatts: configuration.maxPowerWatts,
iconName: configuration.iconName,
colorName: configuration.colorName,
system: system
system: system,
remoteIconURLString: configuration.remoteIconURLString,
componentID: configuration.componentID
)
context.insert(newCharger)
}