more consitancy

This commit is contained in:
Stefan Lange-Hegermann
2025-10-22 22:43:03 +02:00
parent 802b111aa7
commit 6258a6a66f
25 changed files with 448 additions and 260 deletions

View File

@@ -80,6 +80,9 @@
"loads.overview.empty.message" = "Start by adding a load to see system insights."; "loads.overview.empty.message" = "Start by adding a load to see system insights.";
"loads.overview.empty.create" = "Add Load"; "loads.overview.empty.create" = "Add Load";
"loads.overview.empty.library" = "Browse Library"; "loads.overview.empty.library" = "Browse Library";
"loads.library.button" = "Library";
"loads.onboarding.title" = "Add your first component";
"loads.onboarding.subtitle" = "Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations.";
"loads.overview.status.missing_details.title" = "Missing load details"; "loads.overview.status.missing_details.title" = "Missing load details";
"loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations."; "loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations.";
"loads.overview.status.missing_details.singular" = "load"; "loads.overview.status.missing_details.singular" = "load";
@@ -102,6 +105,8 @@
"battery.bank.metric.capacity" = "Capacity"; "battery.bank.metric.capacity" = "Capacity";
"battery.bank.metric.energy" = "Energy"; "battery.bank.metric.energy" = "Energy";
"battery.overview.empty.create" = "Add Battery"; "battery.overview.empty.create" = "Add Battery";
"battery.onboarding.title" = "Add your first battery";
"battery.onboarding.subtitle" = "Track your bank's capacity and chemistry to keep runtime expectations in check.";
"battery.bank.badge.voltage" = "Voltage"; "battery.bank.badge.voltage" = "Voltage";
"battery.bank.badge.capacity" = "Capacity"; "battery.bank.badge.capacity" = "Capacity";
"battery.bank.badge.energy" = "Energy"; "battery.bank.badge.energy" = "Energy";

View File

@@ -232,7 +232,7 @@ struct BatteryEditorView: View {
voltageInput = formattedEditValue(configuration.nominalVoltage) voltageInput = formattedEditValue(configuration.nominalVoltage)
} }
} }
.onChange(of: voltageInput) { newValue in .onChange(of: voltageInput) { _, newValue in
guard editingField == .voltage, let parsed = parseInput(newValue) else { return } guard editingField == .voltage, let parsed = parseInput(newValue) else { return }
configuration.nominalVoltage = roundToTenth(parsed) configuration.nominalVoltage = roundToTenth(parsed)
} }
@@ -306,7 +306,7 @@ struct BatteryEditorView: View {
capacityInput = formattedEditValue(configuration.capacityAmpHours) capacityInput = formattedEditValue(configuration.capacityAmpHours)
} }
} }
.onChange(of: capacityInput) { newValue in .onChange(of: capacityInput) { _, newValue in
guard editingField == .capacity, let parsed = parseInput(newValue) else { return } guard editingField == .capacity, let parsed = parseInput(newValue) else { return }
configuration.capacityAmpHours = roundToTenth(parsed) configuration.capacityAmpHours = roundToTenth(parsed)
} }

View File

@@ -1,122 +0,0 @@
import SwiftUI
struct ComponentsOnboardingView: View {
@State private var carouselStep = 0
let onCreate: () -> Void
let onBrowse: () -> Void
private let imageNames = [
"coffee-onboarding",
"router-onboarding",
"charger-onboarding"
]
private let timer = Timer.publish(every: 8, on: .main, in: .common).autoconnect()
private let animationDuration = 0.8
private var loopingImages: [String] {
guard let first = imageNames.first else { return [] }
return imageNames + [first]
}
var body: some View {
VStack {
Spacer(minLength: 32)
OnboardingCarouselView(images: loopingImages, step: carouselStep)
.frame(minHeight: 80, maxHeight: 240)
.padding(.horizontal, 0)
VStack(spacing: 12) {
Text("Add your first component")
.font(.title2.weight(.semibold))
.multilineTextAlignment(.center)
Text("Bring your system to life with components and let **Cable by VoltPlan** handle cable and fuse recommendations.")
.font(.body)
.foregroundStyle(Color.secondary)
.multilineTextAlignment(.center)
.frame(minHeight: 72)
.padding(.horizontal, 12)
}
.padding(.horizontal, 24)
Spacer()
VStack(spacing: 12) {
Button(action: createComponent) {
HStack(spacing: 8) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 16))
Text("Create Component")
.font(.headline.weight(.semibold))
}
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background(Color.blue)
.cornerRadius(12)
}
.accessibilityIdentifier("create-component-button")
.buttonStyle(.plain)
Button(action: onBrowse) {
HStack(spacing: 8) {
Image(systemName: "books.vertical")
.font(.system(size: 16))
Text("Browse Library")
.font(.headline.weight(.semibold))
}
.foregroundColor(.blue)
.frame(maxWidth: .infinity)
.frame(height: 48)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color.blue.opacity(0.12))
)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(Color.blue.opacity(0.24), lineWidth: 1)
)
}
.accessibilityIdentifier("select-component-button")
.buttonStyle(.plain)
}
.padding(.horizontal, 24)
}
.padding(.bottom, 32)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.onAppear(perform: resetState)
.onReceive(timer) { _ in advanceCarousel() }
}
private func resetState() {
carouselStep = 0
}
private func createComponent() {
onCreate()
}
private func advanceCarousel() {
guard imageNames.count > 1 else { return }
let next = carouselStep + 1
withAnimation(.easeInOut(duration: animationDuration)) {
carouselStep = next
}
if next == imageNames.count {
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
withAnimation(.none) {
carouselStep = 0
}
}
}
}
}
#Preview {
ComponentsOnboardingView(onCreate: {}, onBrowse: {})
}

View File

@@ -151,7 +151,7 @@ struct CalculatorView: View {
lengthInput = formattedValue(calculator.length) lengthInput = formattedValue(calculator.length)
} }
} }
.onChange(of: lengthInput) { newValue in .onChange(of: lengthInput) { _, newValue in
guard editingValue == .length, let parsed = parseInput(newValue) else { return } guard editingValue == .length, let parsed = parseInput(newValue) else { return }
calculator.length = roundToTenth(parsed) calculator.length = roundToTenth(parsed)
} }
@@ -187,7 +187,7 @@ struct CalculatorView: View {
voltageInput = formattedValue(calculator.voltage) voltageInput = formattedValue(calculator.voltage)
} }
} }
.onChange(of: voltageInput) { newValue in .onChange(of: voltageInput) { _, newValue in
guard editingValue == .voltage, let parsed = parseInput(newValue) else { return } guard editingValue == .voltage, let parsed = parseInput(newValue) else { return }
calculator.voltage = roundToTenth(parsed) calculator.voltage = roundToTenth(parsed)
} }
@@ -228,7 +228,7 @@ struct CalculatorView: View {
currentInput = formattedValue(calculator.current) currentInput = formattedValue(calculator.current)
} }
} }
.onChange(of: currentInput) { newValue in .onChange(of: currentInput) { _, newValue in
guard editingValue == .current, let parsed = parseInput(newValue) else { return } guard editingValue == .current, let parsed = parseInput(newValue) else { return }
calculator.current = roundToTenth(parsed) calculator.current = roundToTenth(parsed)
} }
@@ -265,7 +265,7 @@ struct CalculatorView: View {
powerInput = formattedValue(calculator.power) powerInput = formattedValue(calculator.power)
} }
} }
.onChange(of: powerInput) { newValue in .onChange(of: powerInput) { _, newValue in
guard editingValue == .power, let parsed = parseInput(newValue) else { return } guard editingValue == .power, let parsed = parseInput(newValue) else { return }
calculator.power = roundToNearestFive(parsed) calculator.power = roundToNearestFive(parsed)
} }
@@ -824,7 +824,7 @@ struct CalculatorView: View {
unit: "V", unit: "V",
tapAction: beginVoltageEditing, tapAction: beginVoltageEditing,
snapValues: editingValue == .voltage ? nil : voltageSnapValues) snapValues: editingValue == .voltage ? nil : voltageSnapValues)
.onChange(of: calculator.voltage) { .onChange(of: calculator.voltage) { _, _ in
if isWattMode { if isWattMode {
calculator.updateFromPower() calculator.updateFromPower()
} else { } else {
@@ -859,7 +859,7 @@ struct CalculatorView: View {
}, },
tapAction: beginPowerEditing, tapAction: beginPowerEditing,
snapValues: editingValue == .power ? nil : powerSnapValues) snapValues: editingValue == .power ? nil : powerSnapValues)
.onChange(of: calculator.power) { .onChange(of: calculator.power) { _, _ in
calculator.updateFromPower() calculator.updateFromPower()
calculator.objectWillChange.send() calculator.objectWillChange.send()
autoUpdateSavedLoad() autoUpdateSavedLoad()
@@ -886,7 +886,7 @@ struct CalculatorView: View {
}, },
tapAction: beginCurrentEditing, tapAction: beginCurrentEditing,
snapValues: editingValue == .current ? nil : currentSnapValues) snapValues: editingValue == .current ? nil : currentSnapValues)
.onChange(of: calculator.current) { .onChange(of: calculator.current) { _, _ in
calculator.updateFromCurrent() calculator.updateFromCurrent()
calculator.objectWillChange.send() calculator.objectWillChange.send()
autoUpdateSavedLoad() autoUpdateSavedLoad()
@@ -914,7 +914,7 @@ struct CalculatorView: View {
unit: unitSettings.unitSystem.lengthUnit, unit: unitSettings.unitSystem.lengthUnit,
tapAction: beginLengthEditing, tapAction: beginLengthEditing,
snapValues: editingValue == .length ? nil : lengthSnapValues) snapValues: editingValue == .length ? nil : lengthSnapValues)
.onChange(of: calculator.length) { .onChange(of: calculator.length) { _, _ in
calculator.objectWillChange.send() calculator.objectWillChange.send()
autoUpdateSavedLoad() autoUpdateSavedLoad()
} }
@@ -1239,17 +1239,21 @@ struct SliderSection: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
Slider(value: $value, in: range) Slider(value: $value, in: range)
.onChange(of: value) { .onChange(of: value) { _, newValue in
// Always round to 1 decimal place first // Always round to 1 decimal place first
value = round(value * 10) / 10 var adjusted = (newValue * 10).rounded() / 10
if let snapValues = snapValues { if let snapValues = snapValues {
// Find the closest snap value // Find the closest snap value
let closest = snapValues.min { abs($0 - value) < abs($1 - value) } if let closest = snapValues.min(by: { abs($0 - adjusted) < abs($1 - adjusted) }),
if let closest = closest, abs(closest - value) < 0.5 { abs(closest - adjusted) < 0.5 {
value = closest adjusted = closest
} }
} }
if abs(adjusted - newValue) > 0.000001 {
value = adjusted
}
} }
Text(String(format: "%.0f%@", range.upperBound, unit)) Text(String(format: "%.0f%@", range.upperBound, unit))

View File

@@ -126,15 +126,6 @@ struct ComponentLibraryItem: Identifiable, Equatable {
append(locale.identifier) append(locale.identifier)
let components = Locale.components(fromIdentifier: locale.identifier)
if let language = components[NSLocale.Key.languageCode.rawValue]?.lowercased() {
if let region = components[NSLocale.Key.countryCode.rawValue]?.uppercased() {
append("\(language)_\(region)")
}
append(language)
}
if let languageCode = locale.language.languageCode?.identifier.lowercased() { if let languageCode = locale.language.languageCode?.identifier.lowercased() {
append(languageCode) append(languageCode)
} }

View File

@@ -70,6 +70,13 @@ struct LoadsView: View {
systemImage: "square.stack.3d.up" systemImage: "square.stack.3d.up"
) )
} }
Group {
if savedBatteries.isEmpty {
OnboardingInfoView(
configuration: .battery(),
onPrimaryAction: { startBatteryConfiguration() }
)
} else {
BatteriesView( BatteriesView(
system: system, system: system,
batteries: savedBatteries, batteries: savedBatteries,
@@ -78,6 +85,8 @@ struct LoadsView: View {
onDelete: deleteBatteries onDelete: deleteBatteries
) )
.environment(\.editMode, $editMode) .environment(\.editMode, $editMode)
}
}
.tag(ComponentTab.batteries) .tag(ComponentTab.batteries)
.tabItem { .tabItem {
Label( Label(
@@ -89,6 +98,7 @@ struct LoadsView: View {
systemImage: "battery.100" systemImage: "battery.100"
) )
} }
.environment(\.editMode, $editMode)
ChargersView(system: system) ChargersView(system: system)
.tag(ComponentTab.chargers) .tag(ComponentTab.chargers)
.tabItem { .tabItem {
@@ -129,39 +139,31 @@ struct LoadsView: View {
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
let showPrimary = selectedComponentTab != .overview
let showEditLoads = selectedComponentTab == .components && !savedLoads.isEmpty
let showEditBatteries = selectedComponentTab == .batteries && !savedBatteries.isEmpty
if showPrimary || showEditLoads || showEditBatteries {
HStack { HStack {
if selectedComponentTab == .components { if showPrimary {
Button(action: {
showingComponentLibrary = true
}) {
Image(systemName: "books.vertical")
}
.accessibilityIdentifier("component-library-button")
}
if !savedLoads.isEmpty && (selectedComponentTab == .components || selectedComponentTab == .overview) {
Button(action: {
showingSystemBOM = true
}) {
Image(systemName: "list.bullet.rectangle")
}
.accessibilityIdentifier("system-bom-button")
}
Button(action: { Button(action: {
handlePrimaryAction() handlePrimaryAction()
}) { }) {
Image(systemName: "plus") Image(systemName: "plus")
} }
.disabled(selectedComponentTab == .chargers) .disabled(selectedComponentTab == .chargers)
if selectedComponentTab == .components { }
if showEditLoads {
EditButton() EditButton()
.disabled(savedLoads.isEmpty) .disabled(savedLoads.isEmpty)
} else if selectedComponentTab == .batteries { } else if showEditBatteries {
EditButton() EditButton()
.disabled(savedBatteries.isEmpty) .disabled(savedBatteries.isEmpty)
} }
} }
} }
} }
}
.navigationDestination(item: $newLoadToEdit) { load in .navigationDestination(item: $newLoadToEdit) { load in
CalculatorView(savedLoad: load) CalculatorView(savedLoad: load)
} }
@@ -267,6 +269,29 @@ struct LoadsView: View {
Text(loadsSummaryTitle) Text(loadsSummaryTitle)
.font(.headline.weight(.semibold)) .font(.headline.weight(.semibold))
Spacer() Spacer()
Button {
showingComponentLibrary = true
} label: {
HStack(spacing: 6) {
Image(systemName: "books.vertical")
Text(
String(
localized: "loads.library.button",
bundle: .main,
comment: "Button title to open component library"
)
)
}
.font(.footnote.weight(.semibold))
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(
Capsule(style: .continuous)
.fill(Color(.tertiarySystemFill))
)
}
.buttonStyle(.plain)
.foregroundStyle(Color.accentColor)
} }
ViewThatFits(in: .horizontal) { ViewThatFits(in: .horizontal) {
@@ -333,19 +358,16 @@ struct LoadsView: View {
private var componentsTab: some View { private var componentsTab: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
if savedLoads.isEmpty {
OnboardingInfoView(
configuration: .loads(),
onPrimaryAction: { createNewLoad() },
onSecondaryAction: { showingComponentLibrary = true }
)
.padding(.horizontal, 0)
} else {
summarySection summarySection
if savedLoads.isEmpty {
ScrollView {
ComponentsOnboardingView(
onCreate: { createNewLoad() },
onBrowse: { showingComponentLibrary = true }
)
.padding(.horizontal, 16)
.padding(.top, 32)
.padding(.bottom, 24)
}
} else {
List { List {
ForEach(savedLoads) { load in ForEach(savedLoads) { load in
Button { Button {

View File

@@ -0,0 +1,149 @@
import SwiftUI
struct OnboardingInfoView: View {
struct Configuration {
let title: LocalizedStringKey
let subtitle: LocalizedStringKey
let primaryActionTitle: LocalizedStringKey
let primaryActionIcon: String
let secondaryActionTitle: LocalizedStringKey?
let secondaryActionIcon: String?
let imageNames: [String]
}
@State private var carouselStep = 0
private let configuration: Configuration
private let onPrimaryAction: () -> Void
private let onSecondaryAction: () -> Void
private let timer = Timer.publish(every: 8, on: .main, in: .common).autoconnect()
private let animationDuration = 0.8
private var loopingImages: [String] {
guard let first = configuration.imageNames.first else { return [] }
return configuration.imageNames + [first]
}
init(configuration: Configuration, onPrimaryAction: @escaping () -> Void, onSecondaryAction: @escaping () -> Void = {}) {
self.configuration = configuration
self.onPrimaryAction = onPrimaryAction
self.onSecondaryAction = onSecondaryAction
}
var body: some View {
VStack(spacing: 24) {
Spacer(minLength: 32)
if !loopingImages.isEmpty {
OnboardingCarouselView(images: loopingImages, step: carouselStep)
.frame(minHeight: 80, maxHeight: 220)
.padding(.horizontal, 0)
}
VStack(spacing: 12) {
Text(configuration.title)
.font(.title2.weight(.semibold))
.multilineTextAlignment(.center)
Text(configuration.subtitle)
.font(.body)
.foregroundStyle(Color.secondary)
.multilineTextAlignment(.center)
.frame(minHeight: 72)
.padding(.horizontal, 12)
}
.padding(.horizontal, 24)
Spacer()
VStack(spacing: 12) {
Button(action: onPrimaryAction) {
Label(configuration.primaryActionTitle, systemImage: configuration.primaryActionIcon)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
if let secondaryTitle = configuration.secondaryActionTitle,
let secondaryIcon = configuration.secondaryActionIcon {
Button(action: onSecondaryAction) {
Label(secondaryTitle, systemImage: secondaryIcon)
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(.accentColor)
.controlSize(.large)
}
}
.padding(.horizontal, 24)
}
.padding(.vertical, 24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.onAppear(perform: resetState)
.onReceive(timer) { _ in advanceCarousel() }
}
private func resetState() {
carouselStep = 0
}
private func advanceCarousel() {
guard configuration.imageNames.count > 1 else { return }
let next = carouselStep + 1
withAnimation(.easeInOut(duration: animationDuration)) {
carouselStep = next
}
if next == configuration.imageNames.count {
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
withAnimation(.none) {
carouselStep = 0
}
}
}
}
}
#Preview {
OnboardingInfoView(
configuration: .loads(),
onPrimaryAction: {},
onSecondaryAction: {}
)
}
extension OnboardingInfoView.Configuration {
static func loads() -> Self {
Self(
title: LocalizedStringKey("loads.onboarding.title"),
subtitle: LocalizedStringKey("loads.onboarding.subtitle"),
primaryActionTitle: LocalizedStringKey("loads.overview.empty.create"),
primaryActionIcon: "plus",
secondaryActionTitle: LocalizedStringKey("loads.overview.empty.library"),
secondaryActionIcon: "books.vertical",
imageNames: [
"coffee-onboarding",
"router-onboarding",
"charger-onboarding"
]
)
}
static func battery() -> Self {
Self(
title: LocalizedStringKey("battery.onboarding.title"),
subtitle: LocalizedStringKey("battery.onboarding.subtitle"),
primaryActionTitle: LocalizedStringKey("battery.overview.empty.create"),
primaryActionIcon: "plus",
secondaryActionTitle: nil,
secondaryActionIcon: nil,
imageNames: [
"charger-onboarding",
"router-onboarding",
"coffee-onboarding"
]
)
}
}

View File

@@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct SystemOverviewView: View { struct SystemOverviewView: View {
@Environment(\.dismiss) private var dismiss
@State private var activeStatus: LoadConfigurationStatus? @State private var activeStatus: LoadConfigurationStatus?
@State private var suppressLoadNavigation = false @State private var suppressLoadNavigation = false
let system: ElectricalSystem let system: ElectricalSystem
@@ -771,3 +772,85 @@ struct SystemOverviewView: View {
} }
} }
} }
#Preview("SystemOverview Populated") {
let system = ElectricalSystem(
name: "12V DC System",
location: "Engine Room",
iconName: "bolt.circle.fill",
colorName: "blue"
)
let loads: [SavedLoad] = [
SavedLoad(
name: "Navigation Lights",
voltage: 12.8,
current: 2.4,
power: 28.8,
length: 5.0,
crossSection: 2.5
),
SavedLoad(
name: "Bilge Pump",
voltage: 12.8,
current: 8.0,
power: 96.0,
length: 3.0,
crossSection: 4.0
),
SavedLoad(
name: "Chartplotter",
voltage: 12.8,
current: 1.5,
power: 18.0,
length: 2.0,
crossSection: 1.5
)
]
let batteries: [SavedBattery] = [
SavedBattery(
name: "House AGM",
nominalVoltage: 12.0,
capacityAmpHours: 100.0
),
SavedBattery(
name: "Starter AGM",
nominalVoltage: 12.0,
capacityAmpHours: 100.0
)
]
SystemOverviewView(
system: system,
loads: loads,
batteries: batteries,
onSelectLoads: {},
onSelectBatteries: {},
onCreateLoad: {},
onBrowseLibrary: {},
onCreateBattery: {}
)
.padding()
}
#Preview("SystemOverview Empty States") {
let system = ElectricalSystem(
name: "24V DC System",
location: "Main Panel",
iconName: "bolt.circle.fill",
colorName: "green"
)
return SystemOverviewView(
system: system,
loads: [],
batteries: [],
onSelectLoads: {},
onSelectBatteries: {},
onCreateLoad: {},
onBrowseLibrary: {},
onCreateBattery: {}
)
.padding()
}

View File

@@ -1,40 +0,0 @@
//
// SystemView.swift
// Cable
//
// Created by Stefan Lange-Hegermann on 09.10.25.
//
import SwiftUI
import SwiftData
struct SystemView: View {
var body: some View {
NavigationStack {
VStack(spacing: 24) {
Spacer()
VStack(spacing: 16) {
Image(systemName: "square.grid.3x2")
.font(.system(size: 48))
.foregroundColor(.secondary)
Text("System View")
.font(.title2)
.fontWeight(.semibold)
Text("Coming soon - manage your electrical systems and panels here.")
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 48)
}
Spacer()
Spacer()
}
.navigationTitle("System")
}
}
}

View File

@@ -414,3 +414,79 @@ struct SystemsView: View {
} }
} }
} }
#Preview("Sample Systems") {
// An in-memory SwiftData container for previews so we don't persist anything
let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: ElectricalSystem.self, SavedLoad.self, configurations: configuration)
// Seed sample data only once per preview session
if (try? ModelContext(container).fetch(FetchDescriptor<ElectricalSystem>()))?.isEmpty ?? true {
let context = ModelContext(container)
// Sample systems
let system1 = ElectricalSystem(name: "Camper Van", location: "Road Trip", iconName: "bus", colorName: "teal")
let system2 = ElectricalSystem(name: "Sailboat Aurora", location: "Marina 7", iconName: "sailboat", colorName: "blue")
context.insert(system1)
context.insert(system2)
// Sample loads for system 1
let load1 = SavedLoad(
name: "LED Cabin Light",
voltage: 12,
current: 0.5,
power: 6,
length: 5,
crossSection: 1.5,
iconName: "lightbulb",
colorName: "yellow",
isWattMode: false,
system: system1,
remoteIconURLString: nil,
affiliateURLString: nil,
affiliateCountryCode: nil
)
let load2 = SavedLoad(
name: "Water Pump",
voltage: 12,
current: 5,
power: 60,
length: 3,
crossSection: 2.5,
iconName: "drop",
colorName: "blue",
isWattMode: false,
system: system1,
remoteIconURLString: nil,
affiliateURLString: nil,
affiliateCountryCode: nil
)
// Sample loads for system 2
let load3 = SavedLoad(
name: "Navigation Lights",
voltage: 12,
current: 1.2,
power: 14.4,
length: 8,
crossSection: 1.5,
iconName: "lightbulb",
colorName: "green",
isWattMode: false,
system: system2,
remoteIconURLString: nil,
affiliateURLString: nil,
affiliateCountryCode: nil
)
context.insert(load1)
context.insert(load2)
context.insert(load3)
}
return SystemsView()
.modelContainer(container)
.environmentObject(UnitSystemSettings())
}

View File

@@ -141,13 +141,16 @@
"tab.batteries" = "Batterien"; "tab.batteries" = "Batterien";
"tab.chargers" = "Ladegeräte"; "tab.chargers" = "Ladegeräte";
"loads.overview.header.title" = "Verbraucherübersicht"; "loads.overview.header.title" = "Verbraucher";
"loads.overview.metric.count" = "Verbraucher"; "loads.overview.metric.count" = "Verbraucher";
"loads.overview.metric.current" = "Strom"; "loads.overview.metric.current" = "Strom";
"loads.overview.metric.power" = "Leistung"; "loads.overview.metric.power" = "Leistung";
"loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten."; "loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten.";
"loads.overview.empty.create" = "Verbraucher hinzufügen"; "loads.overview.empty.create" = "Verbraucher hinzufügen";
"loads.overview.empty.library" = "Bibliothek durchsuchen"; "loads.overview.empty.library" = "Bibliothek durchsuchen";
"loads.library.button" = "Bibliothek";
"loads.onboarding.title" = "Füge deinen ersten Verbraucher hinzu";
"loads.onboarding.subtitle" = "Statte dein System mit Verbrauchern aus und lass **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen übernehmen.";
"loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails"; "loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails";
"loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten."; "loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten.";
"loads.overview.status.missing_details.singular" = "Verbraucher"; "loads.overview.status.missing_details.singular" = "Verbraucher";
@@ -165,11 +168,13 @@
"battery.bank.warning.voltage.short" = "Spannung"; "battery.bank.warning.voltage.short" = "Spannung";
"battery.bank.warning.capacity.short" = "Kapazität"; "battery.bank.warning.capacity.short" = "Kapazität";
"battery.bank.header.title" = "Batteriebank"; "battery.bank.header.title" = "Batterien";
"battery.bank.metric.count" = "Batterien"; "battery.bank.metric.count" = "Batterien";
"battery.bank.metric.capacity" = "Kapazität"; "battery.bank.metric.capacity" = "Kapazität";
"battery.bank.metric.energy" = "Energie"; "battery.bank.metric.energy" = "Energie";
"battery.overview.empty.create" = "Batterie hinzufügen"; "battery.overview.empty.create" = "Batterie hinzufügen";
"battery.onboarding.title" = "Füge deine erste Batterie hinzu";
"battery.onboarding.subtitle" = "Behalte Kapazität und Chemie deiner Batterien im Blick, um die Laufzeit im Griff zu behalten.";
"battery.bank.badge.voltage" = "Spannung"; "battery.bank.badge.voltage" = "Spannung";
"battery.bank.badge.capacity" = "Kapazität"; "battery.bank.badge.capacity" = "Kapazität";
"battery.bank.badge.energy" = "Energie"; "battery.bank.badge.energy" = "Energie";

View File

@@ -147,6 +147,9 @@
"loads.overview.empty.message" = "Añade una carga para ver los detalles del sistema."; "loads.overview.empty.message" = "Añade una carga para ver los detalles del sistema.";
"loads.overview.empty.create" = "Añadir carga"; "loads.overview.empty.create" = "Añadir carga";
"loads.overview.empty.library" = "Explorar biblioteca"; "loads.overview.empty.library" = "Explorar biblioteca";
"loads.library.button" = "Biblioteca";
"loads.onboarding.title" = "Añade tu primer consumidor";
"loads.onboarding.subtitle" = "Completa tu sistema con consumidores y deja que **Cable by VoltPlan** calcule cables y fusibles por ti.";
"loads.overview.status.missing_details.title" = "Faltan detalles de la carga"; "loads.overview.status.missing_details.title" = "Faltan detalles de la carga";
"loads.overview.status.missing_details.message" = "Introduce la longitud del cable y el calibre del conductor para %d %@ para obtener recomendaciones precisas."; "loads.overview.status.missing_details.message" = "Introduce la longitud del cable y el calibre del conductor para %d %@ para obtener recomendaciones precisas.";
"loads.overview.status.missing_details.singular" = "carga"; "loads.overview.status.missing_details.singular" = "carga";
@@ -169,6 +172,8 @@
"battery.bank.metric.capacity" = "Capacidad"; "battery.bank.metric.capacity" = "Capacidad";
"battery.bank.metric.energy" = "Energía"; "battery.bank.metric.energy" = "Energía";
"battery.overview.empty.create" = "Añadir batería"; "battery.overview.empty.create" = "Añadir batería";
"battery.onboarding.title" = "Añade tu primera batería";
"battery.onboarding.subtitle" = "Controla la capacidad y la química del banco para mantener tus tiempos de autonomía bajo control.";
"battery.bank.badge.voltage" = "Voltaje"; "battery.bank.badge.voltage" = "Voltaje";
"battery.bank.badge.capacity" = "Capacidad"; "battery.bank.badge.capacity" = "Capacidad";
"battery.bank.badge.energy" = "Energía"; "battery.bank.badge.energy" = "Energía";

View File

@@ -147,6 +147,9 @@
"loads.overview.empty.message" = "Ajoutez une charge pour voir les informations du système."; "loads.overview.empty.message" = "Ajoutez une charge pour voir les informations du système.";
"loads.overview.empty.create" = "Ajouter une charge"; "loads.overview.empty.create" = "Ajouter une charge";
"loads.overview.empty.library" = "Parcourir la bibliothèque"; "loads.overview.empty.library" = "Parcourir la bibliothèque";
"loads.library.button" = "Bibliothèque";
"loads.onboarding.title" = "Ajoutez votre premier consommateur";
"loads.onboarding.subtitle" = "Complétez votre système avec des équipements et laissez **Cable by VoltPlan** proposer les câbles et fusibles adaptés.";
"loads.overview.status.missing_details.title" = "Détails de charge manquants"; "loads.overview.status.missing_details.title" = "Détails de charge manquants";
"loads.overview.status.missing_details.message" = "Saisissez la longueur de câble et la section du conducteur pour %d %@ afin d'obtenir des recommandations précises."; "loads.overview.status.missing_details.message" = "Saisissez la longueur de câble et la section du conducteur pour %d %@ afin d'obtenir des recommandations précises.";
"loads.overview.status.missing_details.singular" = "charge"; "loads.overview.status.missing_details.singular" = "charge";
@@ -169,6 +172,8 @@
"battery.bank.metric.capacity" = "Capacité"; "battery.bank.metric.capacity" = "Capacité";
"battery.bank.metric.energy" = "Énergie"; "battery.bank.metric.energy" = "Énergie";
"battery.overview.empty.create" = "Ajouter une batterie"; "battery.overview.empty.create" = "Ajouter une batterie";
"battery.onboarding.title" = "Ajoutez votre première batterie";
"battery.onboarding.subtitle" = "Suivez la capacité et la chimie de votre banc pour mieux maîtriser l'autonomie.";
"battery.bank.badge.voltage" = "Tension"; "battery.bank.badge.voltage" = "Tension";
"battery.bank.badge.capacity" = "Capacité"; "battery.bank.badge.capacity" = "Capacité";
"battery.bank.badge.energy" = "Énergie"; "battery.bank.badge.energy" = "Énergie";

View File

@@ -147,6 +147,9 @@
"loads.overview.empty.message" = "Voeg een belasting toe om systeeminformatie te zien."; "loads.overview.empty.message" = "Voeg een belasting toe om systeeminformatie te zien.";
"loads.overview.empty.create" = "Belasting toevoegen"; "loads.overview.empty.create" = "Belasting toevoegen";
"loads.overview.empty.library" = "Bibliotheek bekijken"; "loads.overview.empty.library" = "Bibliotheek bekijken";
"loads.library.button" = "Bibliotheek";
"loads.onboarding.title" = "Voeg je eerste verbruiker toe";
"loads.onboarding.subtitle" = "Bouw je systeem uit met verbruikers en laat **Cable by VoltPlan** de kabel- en zekeringadviezen verzorgen.";
"loads.overview.status.missing_details.title" = "Ontbrekende lastdetails"; "loads.overview.status.missing_details.title" = "Ontbrekende lastdetails";
"loads.overview.status.missing_details.message" = "Voer kabellengte en kabeldoorsnede in voor %d %@ om nauwkeurige aanbevelingen te krijgen."; "loads.overview.status.missing_details.message" = "Voer kabellengte en kabeldoorsnede in voor %d %@ om nauwkeurige aanbevelingen te krijgen.";
"loads.overview.status.missing_details.singular" = "last"; "loads.overview.status.missing_details.singular" = "last";
@@ -169,6 +172,8 @@
"battery.bank.metric.capacity" = "Capaciteit"; "battery.bank.metric.capacity" = "Capaciteit";
"battery.bank.metric.energy" = "Energie"; "battery.bank.metric.energy" = "Energie";
"battery.overview.empty.create" = "Accu toevoegen"; "battery.overview.empty.create" = "Accu toevoegen";
"battery.onboarding.title" = "Voeg je eerste accu toe";
"battery.onboarding.subtitle" = "Houd capaciteit en chemie van de accubank in de gaten om de gebruiksduur te beheersen.";
"battery.bank.badge.voltage" = "Spanning"; "battery.bank.badge.voltage" = "Spanning";
"battery.bank.badge.capacity" = "Capaciteit"; "battery.bank.badge.capacity" = "Capaciteit";
"battery.bank.badge.energy" = "Energie"; "battery.bank.badge.energy" = "Energie";