more consitancy
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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: {})
|
|
||||||
}
|
|
||||||
@@ -151,10 +151,10 @@ 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)
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
editingValue = nil
|
editingValue = nil
|
||||||
lengthInput = ""
|
lengthInput = ""
|
||||||
@@ -187,10 +187,10 @@ 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)
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
editingValue = nil
|
editingValue = nil
|
||||||
voltageInput = ""
|
voltageInput = ""
|
||||||
@@ -228,10 +228,10 @@ 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)
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
editingValue = nil
|
editingValue = nil
|
||||||
currentInput = ""
|
currentInput = ""
|
||||||
@@ -265,10 +265,10 @@ 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)
|
||||||
}
|
}
|
||||||
Button("Cancel", role: .cancel) {
|
Button("Cancel", role: .cancel) {
|
||||||
editingValue = nil
|
editingValue = nil
|
||||||
powerInput = ""
|
powerInput = ""
|
||||||
@@ -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,10 +914,10 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func normalizedVoltage(for value: Double) -> Double {
|
private func normalizedVoltage(for value: Double) -> Double {
|
||||||
@@ -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))
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -70,25 +70,35 @@ struct LoadsView: View {
|
|||||||
systemImage: "square.stack.3d.up"
|
systemImage: "square.stack.3d.up"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BatteriesView(
|
Group {
|
||||||
system: system,
|
if savedBatteries.isEmpty {
|
||||||
batteries: savedBatteries,
|
OnboardingInfoView(
|
||||||
editMode: $editMode,
|
configuration: .battery(),
|
||||||
onEdit: { editBattery($0) },
|
onPrimaryAction: { startBatteryConfiguration() }
|
||||||
onDelete: deleteBatteries
|
)
|
||||||
|
} else {
|
||||||
|
BatteriesView(
|
||||||
|
system: system,
|
||||||
|
batteries: savedBatteries,
|
||||||
|
editMode: $editMode,
|
||||||
|
onEdit: { editBattery($0) },
|
||||||
|
onDelete: deleteBatteries
|
||||||
|
)
|
||||||
|
.environment(\.editMode, $editMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tag(ComponentTab.batteries)
|
||||||
|
.tabItem {
|
||||||
|
Label(
|
||||||
|
String(
|
||||||
|
localized: "tab.batteries",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Tab title for battery configurations"
|
||||||
|
),
|
||||||
|
systemImage: "battery.100"
|
||||||
)
|
)
|
||||||
.environment(\.editMode, $editMode)
|
}
|
||||||
.tag(ComponentTab.batteries)
|
.environment(\.editMode, $editMode)
|
||||||
.tabItem {
|
|
||||||
Label(
|
|
||||||
String(
|
|
||||||
localized: "tab.batteries",
|
|
||||||
bundle: .main,
|
|
||||||
comment: "Tab title for battery configurations"
|
|
||||||
),
|
|
||||||
systemImage: "battery.100"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ChargersView(system: system)
|
ChargersView(system: system)
|
||||||
.tag(ComponentTab.chargers)
|
.tag(ComponentTab.chargers)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
@@ -129,35 +139,27 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
HStack {
|
let showPrimary = selectedComponentTab != .overview
|
||||||
if selectedComponentTab == .components {
|
let showEditLoads = selectedComponentTab == .components && !savedLoads.isEmpty
|
||||||
Button(action: {
|
let showEditBatteries = selectedComponentTab == .batteries && !savedBatteries.isEmpty
|
||||||
showingComponentLibrary = true
|
|
||||||
}) {
|
if showPrimary || showEditLoads || showEditBatteries {
|
||||||
Image(systemName: "books.vertical")
|
HStack {
|
||||||
|
if showPrimary {
|
||||||
|
Button(action: {
|
||||||
|
handlePrimaryAction()
|
||||||
|
}) {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
.disabled(selectedComponentTab == .chargers)
|
||||||
}
|
}
|
||||||
.accessibilityIdentifier("component-library-button")
|
if showEditLoads {
|
||||||
}
|
EditButton()
|
||||||
if !savedLoads.isEmpty && (selectedComponentTab == .components || selectedComponentTab == .overview) {
|
.disabled(savedLoads.isEmpty)
|
||||||
Button(action: {
|
} else if showEditBatteries {
|
||||||
showingSystemBOM = true
|
EditButton()
|
||||||
}) {
|
.disabled(savedBatteries.isEmpty)
|
||||||
Image(systemName: "list.bullet.rectangle")
|
|
||||||
}
|
}
|
||||||
.accessibilityIdentifier("system-bom-button")
|
|
||||||
}
|
|
||||||
Button(action: {
|
|
||||||
handlePrimaryAction()
|
|
||||||
}) {
|
|
||||||
Image(systemName: "plus")
|
|
||||||
}
|
|
||||||
.disabled(selectedComponentTab == .chargers)
|
|
||||||
if selectedComponentTab == .components {
|
|
||||||
EditButton()
|
|
||||||
.disabled(savedLoads.isEmpty)
|
|
||||||
} else if selectedComponentTab == .batteries {
|
|
||||||
EditButton()
|
|
||||||
.disabled(savedBatteries.isEmpty)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
summarySection
|
|
||||||
|
|
||||||
if savedLoads.isEmpty {
|
if savedLoads.isEmpty {
|
||||||
ScrollView {
|
OnboardingInfoView(
|
||||||
ComponentsOnboardingView(
|
configuration: .loads(),
|
||||||
onCreate: { createNewLoad() },
|
onPrimaryAction: { createNewLoad() },
|
||||||
onBrowse: { showingComponentLibrary = true }
|
onSecondaryAction: { showingComponentLibrary = true }
|
||||||
)
|
)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 0)
|
||||||
.padding(.top, 32)
|
|
||||||
.padding(.bottom, 24)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
summarySection
|
||||||
|
|
||||||
List {
|
List {
|
||||||
ForEach(savedLoads) { load in
|
ForEach(savedLoads) { load in
|
||||||
Button {
|
Button {
|
||||||
149
Cable/Loads/OnboardingInfoView.swift
Normal file
149
Cable/Loads/OnboardingInfoView.swift
Normal 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"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user