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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user