some advanced settings

This commit is contained in:
Stefan Lange-Hegermann
2025-10-28 13:31:51 +01:00
parent 0989c68aa7
commit 46664625b4
13 changed files with 815 additions and 219 deletions

View File

@@ -32,6 +32,18 @@
"slider.length.title" = "Cable Length (%@)";
"slider.power.title" = "Power";
"slider.voltage.title" = "Voltage";
"calculator.advanced.section.title" = "Advanced Settings";
"calculator.advanced.duty_cycle.title" = "Duty Cycle";
"calculator.advanced.duty_cycle.helper" = "Percentage of each active session where the load actually draws power.";
"calculator.advanced.usage_hours.title" = "Daily On-Time";
"calculator.advanced.usage_hours.helper" = "Hours per day the load is turned on.";
"calculator.advanced.usage_hours.unit" = "h/day";
"calculator.alert.duty_cycle.title" = "Edit Duty Cycle";
"calculator.alert.duty_cycle.placeholder" = "Duty Cycle";
"calculator.alert.duty_cycle.message" = "Enter duty cycle as a percentage (0-100%).";
"calculator.alert.usage_hours.title" = "Edit Daily On-Time";
"calculator.alert.usage_hours.placeholder" = "Daily On-Time";
"calculator.alert.usage_hours.message" = "Enter the number of hours per day the load is active.";
"system.list.no.components" = "No components yet";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metric (mm², m)";
@@ -104,6 +116,8 @@
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacity";
"battery.bank.metric.energy" = "Energy";
"battery.bank.metric.usable_capacity" = "Usable Capacity";
"battery.bank.metric.usable_energy" = "Usable Energy";
"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.";
@@ -135,12 +149,20 @@
"battery.editor.section.summary" = "Summary";
"battery.editor.slider.voltage" = "Nominal Voltage";
"battery.editor.slider.capacity" = "Capacity";
"battery.editor.slider.usable_capacity" = "Usable Capacity (%)";
"battery.editor.section.advanced" = "Advanced";
"battery.editor.button.reset_default" = "Reset";
"battery.editor.advanced.usable_capacity.footer_default" = "Defaults to %@ based on chemistry.";
"battery.editor.advanced.usable_capacity.footer_override" = "Override active. Chemistry default remains %@.";
"battery.editor.alert.voltage.title" = "Edit Nominal Voltage";
"battery.editor.alert.voltage.placeholder" = "Voltage";
"battery.editor.alert.voltage.message" = "Enter voltage in volts (V)";
"battery.editor.alert.capacity.title" = "Edit Capacity";
"battery.editor.alert.capacity.placeholder" = "Capacity";
"battery.editor.alert.capacity.message" = "Enter capacity in amp-hours (Ah)";
"battery.editor.alert.usable_capacity.title" = "Edit Usable Capacity";
"battery.editor.alert.usable_capacity.placeholder" = "Usable Capacity (%)";
"battery.editor.alert.usable_capacity.message" = "Enter usable capacity percentage (%)";
"battery.editor.alert.cancel" = "Cancel";
"battery.editor.alert.save" = "Save";
"battery.editor.default_name" = "New Battery";

View File

@@ -86,6 +86,14 @@ struct BatteriesView: View {
)
}
private var metricUsableCapacityLabel: String {
String(
localized: "battery.bank.metric.usable_capacity",
bundle: .main,
comment: "Label for usable capacity metric"
)
}
private var badgeVoltageLabel: String {
String(
localized: "battery.bank.badge.voltage",
@@ -110,6 +118,14 @@ struct BatteriesView: View {
)
}
private var badgeUsableCapacityLabel: String {
String(
localized: "battery.bank.metric.usable_capacity",
bundle: .main,
comment: "Label for usable capacity badge"
)
}
private var emptyTitle: String {
String(
localized: "battery.bank.empty.title",
@@ -190,49 +206,15 @@ struct BatteriesView: View {
Spacer()
}
ViewThatFits(in: .horizontal) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
}
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "battery.100",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "bolt.circle",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
ForEach(Array(summaryMetrics.enumerated()), id: \.offset) { _, metric in
summaryMetric(icon: metric.icon, label: metric.label, value: metric.value, tint: metric.tint)
}
}
.padding(.horizontal, 2)
}
.scrollClipDisabled(false)
if let status = bankStatus {
Button {
@@ -285,24 +267,7 @@ struct BatteriesView: View {
.foregroundStyle(.secondary)
}
HStack(spacing: 12) {
metricBadge(
label: badgeVoltageLabel,
value: formattedValue(battery.nominalVoltage, unit: "V"),
tint: .orange
)
metricBadge(
label: badgeCapacityLabel,
value: formattedValue(battery.capacityAmpHours, unit: "Ah"),
tint: .blue
)
metricBadge(
label: badgeEnergyLabel,
value: formattedValue(battery.energyWattHours, unit: "Wh"),
tint: .green
)
Spacer()
}
batteryMetricsScroll(for: battery)
}
.padding(.vertical, 16)
.padding(.horizontal, 16)
@@ -335,6 +300,27 @@ struct BatteriesView: View {
}
}
private var totalUsableCapacity: Double {
batteries.reduce(0) { result, battery in
result + battery.usableCapacityAmpHours
}
}
private var totalUsableCapacityShare: Double {
guard totalCapacity > 0 else { return 0 }
return max(0, min(1, totalUsableCapacity / totalCapacity))
}
private func usableFraction(for battery: SavedBattery) -> Double {
guard battery.capacityAmpHours > 0 else { return 0 }
return max(0, min(1, battery.usableCapacityAmpHours / battery.capacityAmpHours))
}
private func usableCapacityDisplay(for battery: SavedBattery) -> String {
let fraction = usableFraction(for: battery)
return "\(formattedValue(battery.usableCapacityAmpHours, unit: "Ah")) (\(formattedPercentage(fraction)))"
}
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
ComponentSummaryMetricView(
icon: icon,
@@ -344,6 +330,55 @@ struct BatteriesView: View {
)
}
private var summaryMetrics: [(icon: String, label: String, value: String, tint: Color)] {
[
(
icon: "battery.100",
label: metricCountLabel,
value: "\(batteries.count)",
tint: .blue
),
(
icon: "gauge.medium",
label: metricCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
),
(
icon: "battery.100.bolt",
label: metricUsableCapacityLabel,
value: "\(formattedValue(totalUsableCapacity, unit: "Ah")) (\(formattedPercentage(totalUsableCapacityShare)))",
tint: .purple
),
(
icon: "bolt.circle",
label: metricEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
)
]
}
@ViewBuilder
private func batteryMetricsScroll(for battery: SavedBattery) -> some View {
let badges: [(String, String, Color)] = [
(badgeVoltageLabel, formattedValue(battery.nominalVoltage, unit: "V"), .orange),
(badgeCapacityLabel, formattedValue(battery.capacityAmpHours, unit: "Ah"), .blue),
(badgeUsableCapacityLabel, usableCapacityDisplay(for: battery), .purple),
(badgeEnergyLabel, formattedValue(battery.energyWattHours, unit: "Wh"), .green)
]
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(Array(badges.enumerated()), id: \.offset) { _, badge in
ComponentMetricBadgeView(label: badge.0, value: badge.1, tint: badge.2)
}
}
.padding(.horizontal, 2)
}
.scrollClipDisabled(false)
}
private func metricBadge(label: String, value: String, tint: Color) -> some View {
ComponentMetricBadgeView(
label: label,
@@ -401,6 +436,13 @@ struct BatteriesView: View {
return "\(numberString) \(unit)"
}
private func formattedPercentage(_ fraction: Double) -> String {
let clamped = max(0, min(1, fraction))
let percent = clamped * 100
let numberString = Self.numberFormatter.string(from: NSNumber(value: percent)) ?? String(format: "%.1f", percent)
return "\(numberString) %"
}
private var dominantVoltage: Double? {
guard batteries.count > 1 else { return nil }
return dominantValue(

View File

@@ -14,12 +14,28 @@ struct BatteryConfiguration: Identifiable, Hashable {
var displayName: String {
rawValue
}
var usableCapacityFraction: Double {
switch self {
case .floodedLeadAcid:
return 0.5
case .agm:
return 0.5
case .gel:
return 0.6
case .lithiumIronPhosphate:
return 0.9
case .lithiumIon:
return 0.85
}
}
}
let id: UUID
var name: String
var nominalVoltage: Double
var capacityAmpHours: Double
var usableCapacityOverrideFraction: Double?
var chemistry: Chemistry
var iconName: String
var colorName: String
@@ -31,6 +47,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100,
chemistry: Chemistry = .lithiumIronPhosphate,
usableCapacityOverrideFraction: Double? = nil,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem
@@ -39,6 +56,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.name = name
self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours
self.usableCapacityOverrideFraction = usableCapacityOverrideFraction
self.chemistry = chemistry
self.iconName = iconName
self.colorName = colorName
@@ -50,6 +68,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.name = savedBattery.name
self.nominalVoltage = savedBattery.nominalVoltage
self.capacityAmpHours = savedBattery.capacityAmpHours
self.usableCapacityOverrideFraction = savedBattery.usableCapacityOverrideFraction
self.chemistry = savedBattery.chemistry
self.iconName = savedBattery.iconName
self.colorName = savedBattery.colorName
@@ -60,10 +79,34 @@ struct BatteryConfiguration: Identifiable, Hashable {
nominalVoltage * capacityAmpHours
}
var defaultUsableCapacityFraction: Double {
chemistry.usableCapacityFraction
}
var usableCapacityFraction: Double {
if let override = usableCapacityOverrideFraction {
return max(0, min(1, override))
}
return defaultUsableCapacityFraction
}
var defaultUsableCapacityAmpHours: Double {
capacityAmpHours * defaultUsableCapacityFraction
}
var usableCapacityAmpHours: Double {
capacityAmpHours * usableCapacityFraction
}
var usableEnergyWattHours: Double {
usableCapacityAmpHours * nominalVoltage
}
func apply(to savedBattery: SavedBattery) {
savedBattery.name = name
savedBattery.nominalVoltage = nominalVoltage
savedBattery.capacityAmpHours = capacityAmpHours
savedBattery.usableCapacityOverrideFraction = usableCapacityOverrideFraction
savedBattery.chemistry = chemistry
savedBattery.iconName = iconName
savedBattery.colorName = colorName
@@ -78,6 +121,7 @@ extension BatteryConfiguration {
lhs.name == rhs.name &&
lhs.nominalVoltage == rhs.nominalVoltage &&
lhs.capacityAmpHours == rhs.capacityAmpHours &&
lhs.usableCapacityOverrideFraction == rhs.usableCapacityOverrideFraction &&
lhs.chemistry == rhs.chemistry &&
lhs.iconName == rhs.iconName &&
lhs.colorName == rhs.colorName
@@ -88,6 +132,7 @@ extension BatteryConfiguration {
hasher.combine(name)
hasher.combine(nominalVoltage)
hasher.combine(capacityAmpHours)
hasher.combine(usableCapacityOverrideFraction)
hasher.combine(chemistry)
hasher.combine(iconName)
hasher.combine(colorName)

View File

@@ -14,6 +14,8 @@ class CableCalculator: ObservableObject {
@Published var power: Double = 60.0
@Published var length: Double = 10.0
@Published var loadName: String = String(localized: "default.load.name", comment: "Default placeholder name for a load")
@Published var dutyCyclePercent: Double = 100.0
@Published var dailyUsageHours: Double = 1.0
var calculatedPower: Double {
voltage * current
@@ -132,6 +134,8 @@ class SavedLoad {
var iconName: String = "lightbulb"
var colorName: String = "blue"
var isWattMode: Bool = false
var dutyCyclePercent: Double = 100.0
var dailyUsageHours: Double = 1.0
var system: ElectricalSystem?
var remoteIconURLString: String? = nil
var affiliateURLString: String? = nil
@@ -139,7 +143,25 @@ class SavedLoad {
var bomCompletedItemIDs: [String] = []
var identifier: String = UUID().uuidString
init(name: String, voltage: Double, current: Double, power: Double, length: Double, crossSection: Double, iconName: String = "lightbulb", colorName: String = "blue", isWattMode: Bool = false, system: ElectricalSystem? = nil, remoteIconURLString: String? = nil, affiliateURLString: String? = nil, affiliateCountryCode: String? = nil, bomCompletedItemIDs: [String] = [], identifier: String = UUID().uuidString) {
init(
name: String,
voltage: Double,
current: Double,
power: Double,
length: Double,
crossSection: Double,
iconName: String = "lightbulb",
colorName: String = "blue",
isWattMode: Bool = false,
dutyCyclePercent: Double = 100.0,
dailyUsageHours: Double = 1.0,
system: ElectricalSystem? = nil,
remoteIconURLString: String? = nil,
affiliateURLString: String? = nil,
affiliateCountryCode: String? = nil,
bomCompletedItemIDs: [String] = [],
identifier: String = UUID().uuidString
) {
self.name = name
self.voltage = voltage
self.current = current
@@ -150,6 +172,8 @@ class SavedLoad {
self.iconName = iconName
self.colorName = colorName
self.isWattMode = isWattMode
self.dutyCyclePercent = dutyCyclePercent
self.dailyUsageHours = dailyUsageHours
self.system = system
self.remoteIconURLString = remoteIconURLString
self.affiliateURLString = affiliateURLString

View File

@@ -22,6 +22,8 @@ struct CalculatorView: View {
@State private var currentInput: String = ""
@State private var powerInput: String = ""
@State private var lengthInput: String = ""
@State private var dutyCycleInput: String = ""
@State private var usageHoursInput: String = ""
@State private var showingLoadEditor = false
@State private var presentedAffiliateLink: AffiliateLinkInfo?
@State private var completedItemIDs: Set<String>
@@ -34,7 +36,7 @@ struct CalculatorView: View {
}
enum EditingValue {
case voltage, current, power, length
case voltage, current, power, length, dutyCycle, usageHours
}
private static let editFormatter: NumberFormatter = {
@@ -69,72 +71,24 @@ struct CalculatorView: View {
}
var body: some View {
VStack(spacing: 0) {
badgesSection
circuitDiagram
resultsSection
mainContent
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("")
.toolbar {
ToolbarItem(placement: .principal) {
navigationTitle
}
if savedLoad == nil {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveCurrentLoad()
}
}
}
}
.sheet(isPresented: $showingLibrary) {
LoadLibraryView(calculator: calculator)
}
.sheet(isPresented: $showingLoadEditor) {
LoadEditorView(
loadName: Binding(
get: { calculator.loadName },
set: {
calculator.loadName = $0
autoUpdateSavedLoad()
}
),
iconName: Binding(
get: { savedLoad?.iconName ?? "lightbulb" },
set: { newValue in
guard let savedLoad else { return }
savedLoad.iconName = newValue
savedLoad.remoteIconURLString = nil
autoUpdateSavedLoad()
}
),
colorName: Binding(
get: { savedLoad?.colorName ?? "blue" },
set: {
savedLoad?.colorName = $0
autoUpdateSavedLoad()
}
),
remoteIconURLString: Binding(
get: { savedLoad?.remoteIconURLString },
set: { newValue in
guard let savedLoad else { return }
savedLoad.remoteIconURLString = newValue
autoUpdateSavedLoad()
}
)
attachAlerts(
attachSheets(
navigationWrapped(mainLayout)
)
}
.sheet(item: $presentedAffiliateLink) { info in
BillOfMaterialsView(
info: info,
items: buildBillOfMaterialsItems(deviceLink: info.affiliateURL),
completedItemIDs: $completedItemIDs
)
}
)
}
private func attachAlerts<V: View>(_ view: V) -> some View {
let withLength = addLengthAlert(to: view)
let withVoltage = addVoltageAlert(to: withLength)
let withCurrent = addCurrentAlert(to: withVoltage)
let withPower = addPowerAlert(to: withCurrent)
let withDutyCycle = addDutyCycleAlert(to: withPower)
return addUsageHoursAlert(to: withDutyCycle)
}
private func addLengthAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Length", isPresented: Binding(
get: { editingValue == .length },
set: { isPresented in
@@ -151,10 +105,10 @@ struct CalculatorView: View {
lengthInput = formattedValue(calculator.length)
}
}
.onChange(of: lengthInput) { _, newValue in
guard editingValue == .length, let parsed = parseInput(newValue) else { return }
calculator.length = roundToTenth(parsed)
}
.onChange(of: lengthInput) { _, newValue in
guard editingValue == .length, let parsed = parseInput(newValue) else { return }
calculator.length = roundToTenth(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
lengthInput = ""
@@ -171,6 +125,10 @@ struct CalculatorView: View {
} message: {
Text("Enter length in \(unitSettings.unitSystem.lengthUnit)")
}
}
private func addVoltageAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Voltage", isPresented: Binding(
get: { editingValue == .voltage },
set: { isPresented in
@@ -187,10 +145,10 @@ struct CalculatorView: View {
voltageInput = formattedValue(calculator.voltage)
}
}
.onChange(of: voltageInput) { _, newValue in
guard editingValue == .voltage, let parsed = parseInput(newValue) else { return }
calculator.voltage = roundToTenth(parsed)
}
.onChange(of: voltageInput) { _, newValue in
guard editingValue == .voltage, let parsed = parseInput(newValue) else { return }
calculator.voltage = roundToTenth(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
voltageInput = ""
@@ -212,6 +170,10 @@ struct CalculatorView: View {
} message: {
Text("Enter voltage in volts (V)")
}
}
private func addCurrentAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Current", isPresented: Binding(
get: { editingValue == .current },
set: { isPresented in
@@ -228,10 +190,10 @@ struct CalculatorView: View {
currentInput = formattedValue(calculator.current)
}
}
.onChange(of: currentInput) { _, newValue in
guard editingValue == .current, let parsed = parseInput(newValue) else { return }
calculator.current = roundToTenth(parsed)
}
.onChange(of: currentInput) { _, newValue in
guard editingValue == .current, let parsed = parseInput(newValue) else { return }
calculator.current = roundToTenth(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
currentInput = ""
@@ -249,6 +211,10 @@ struct CalculatorView: View {
} message: {
Text("Enter current in amperes (A)")
}
}
private func addPowerAlert<V: View>(to view: V) -> some View {
view
.alert("Edit Power", isPresented: Binding(
get: { editingValue == .power },
set: { isPresented in
@@ -265,10 +231,10 @@ struct CalculatorView: View {
powerInput = formattedValue(calculator.power)
}
}
.onChange(of: powerInput) { _, newValue in
guard editingValue == .power, let parsed = parseInput(newValue) else { return }
calculator.power = roundToNearestFive(parsed)
}
.onChange(of: powerInput) { _, newValue in
guard editingValue == .power, let parsed = parseInput(newValue) else { return }
calculator.power = roundToNearestFive(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
powerInput = ""
@@ -286,14 +252,189 @@ struct CalculatorView: View {
} message: {
Text("Enter power in watts (W)")
}
.onAppear {
if let savedLoad = savedLoad {
loadConfiguration(from: savedLoad)
}
private func addDutyCycleAlert<V: View>(to view: V) -> some View {
view
.alert(
dutyCycleAlertTitle,
isPresented: Binding(
get: { editingValue == .dutyCycle },
set: { isPresented in
if !isPresented {
editingValue = nil
dutyCycleInput = ""
}
}
)
) {
TextField(dutyCycleAlertPlaceholder, text: $dutyCycleInput)
.keyboardType(.decimalPad)
.onAppear {
if dutyCycleInput.isEmpty {
dutyCycleInput = formattedValue(calculator.dutyCyclePercent)
}
}
.onChange(of: dutyCycleInput) { _, newValue in
guard editingValue == .dutyCycle, let parsed = parseInput(newValue) else { return }
calculator.dutyCyclePercent = clampDutyCyclePercent(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
dutyCycleInput = ""
}
Button("Save") {
if let parsed = parseInput(dutyCycleInput) {
calculator.dutyCyclePercent = clampDutyCyclePercent(parsed)
}
editingValue = nil
dutyCycleInput = ""
calculator.objectWillChange.send()
autoUpdateSavedLoad()
}
} message: {
Text(dutyCycleAlertMessage)
}
}
private func addUsageHoursAlert<V: View>(to view: V) -> some View {
view
.alert(
usageHoursAlertTitle,
isPresented: Binding(
get: { editingValue == .usageHours },
set: { isPresented in
if !isPresented {
editingValue = nil
usageHoursInput = ""
}
}
)
) {
TextField(usageHoursAlertPlaceholder, text: $usageHoursInput)
.keyboardType(.decimalPad)
.onAppear {
if usageHoursInput.isEmpty {
usageHoursInput = formattedValue(calculator.dailyUsageHours)
}
}
.onChange(of: usageHoursInput) { _, newValue in
guard editingValue == .usageHours, let parsed = parseInput(newValue) else { return }
calculator.dailyUsageHours = clampUsageHours(parsed)
}
Button("Cancel", role: .cancel) {
editingValue = nil
usageHoursInput = ""
}
Button("Save") {
if let parsed = parseInput(usageHoursInput) {
calculator.dailyUsageHours = clampUsageHours(parsed)
}
editingValue = nil
usageHoursInput = ""
calculator.objectWillChange.send()
autoUpdateSavedLoad()
}
} message: {
Text(usageHoursAlertMessage)
}
}
private func attachSheets<V: View>(_ view: V) -> some View {
view
.sheet(isPresented: $showingLibrary, content: librarySheet)
.sheet(isPresented: $showingLoadEditor, content: loadEditorSheet)
.sheet(item: $presentedAffiliateLink, content: billOfMaterialsSheet(info:))
.onAppear {
if let savedLoad = savedLoad {
loadConfiguration(from: savedLoad)
}
}
.onChange(of: completedItemIDs) { _, _ in
persistCompletedItems()
}
}
private func navigationWrapped<V: View>(_ view: V) -> some View {
view
.navigationBarTitleDisplayMode(.inline)
.navigationTitle("")
.toolbar { toolbarContent }
}
private var mainLayout: some View {
VStack(spacing: 0) {
badgesSection
circuitDiagram
resultsSection
mainContent
}
}
@ToolbarContentBuilder
private var toolbarContent: some ToolbarContent {
ToolbarItem(placement: .principal) {
navigationTitle
}
if savedLoad == nil {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveCurrentLoad()
}
}
}
.onChange(of: completedItemIDs) { _, _ in
persistCompletedItems()
}
}
@ViewBuilder
private func billOfMaterialsSheet(info: AffiliateLinkInfo) -> some View {
BillOfMaterialsView(
info: info,
items: buildBillOfMaterialsItems(deviceLink: info.affiliateURL),
completedItemIDs: $completedItemIDs
)
}
@ViewBuilder
private func librarySheet() -> some View {
LoadLibraryView(calculator: calculator)
}
@ViewBuilder
private func loadEditorSheet() -> some View {
LoadEditorView(
loadName: Binding(
get: { calculator.loadName },
set: {
calculator.loadName = $0
autoUpdateSavedLoad()
}
),
iconName: Binding(
get: { savedLoad?.iconName ?? "lightbulb" },
set: { newValue in
guard let savedLoad else { return }
savedLoad.iconName = newValue
savedLoad.remoteIconURLString = nil
autoUpdateSavedLoad()
}
),
colorName: Binding(
get: { savedLoad?.colorName ?? "blue" },
set: {
savedLoad?.colorName = $0
autoUpdateSavedLoad()
}
),
remoteIconURLString: Binding(
get: { savedLoad?.remoteIconURLString },
set: { newValue in
guard let savedLoad else { return }
savedLoad.remoteIconURLString = newValue
autoUpdateSavedLoad()
}
)
)
}
private var loadIcon: String {
@@ -596,15 +737,20 @@ struct CalculatorView: View {
}
private var mainContent: some View {
ScrollView {
VStack(spacing: 20) {
Divider().padding(.horizontal)
slidersSection
if let info = affiliateLinkInfo {
affiliateLinkSection(info: info)
}
List {
slidersSection
advancedSettingsSection
if let info = affiliateLinkInfo {
affiliateLinkSection(info: info)
.listRowBackground(Color.clear)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
}
.listStyle(.plain)
.scrollIndicators(.hidden)
.scrollContentBackground(.hidden)
.background(Color(.systemGroupedBackground))
}
@ViewBuilder
@@ -640,7 +786,6 @@ struct CalculatorView: View {
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.padding(.horizontal)
}
private func buildBillOfMaterialsItems(deviceLink: URL?) -> [BOMItem] {
@@ -758,13 +903,118 @@ struct CalculatorView: View {
return items
}
private var advancedSettingsTitle: String {
String(
localized: "calculator.advanced.section.title",
comment: "Title for the advanced load settings section"
)
}
private var dutyCycleTitle: String {
String(
localized: "calculator.advanced.duty_cycle.title",
comment: "Title for the duty cycle slider"
)
}
private var dutyCycleHelperText: String {
String(
localized: "calculator.advanced.duty_cycle.helper",
comment: "Helper text explaining duty cycle"
)
}
private var usageHoursTitle: String {
String(
localized: "calculator.advanced.usage_hours.title",
comment: "Title for the daily usage slider"
)
}
private var usageHoursHelperText: String {
String(
localized: "calculator.advanced.usage_hours.helper",
comment: "Helper text explaining daily usage hours"
)
}
private var dutyCycleAlertTitle: String {
String(
localized: "calculator.alert.duty_cycle.title",
comment: "Title for the duty cycle edit alert"
)
}
private var dutyCycleAlertPlaceholder: String {
String(
localized: "calculator.alert.duty_cycle.placeholder",
comment: "Placeholder for the duty cycle alert text field"
)
}
private var dutyCycleAlertMessage: String {
String(
localized: "calculator.alert.duty_cycle.message",
comment: "Helper message for the duty cycle alert"
)
}
private var usageHoursAlertTitle: String {
String(
localized: "calculator.alert.usage_hours.title",
comment: "Title for the daily usage edit alert"
)
}
private var usageHoursAlertPlaceholder: String {
String(
localized: "calculator.alert.usage_hours.placeholder",
comment: "Placeholder for the daily usage alert text field"
)
}
private var usageHoursAlertMessage: String {
String(
localized: "calculator.alert.usage_hours.message",
comment: "Helper message for the daily usage alert"
)
}
private var slidersSection: some View {
VStack(spacing: 30) {
Section {
voltageSlider
.listRowSeparator(.hidden)
currentPowerSlider
.listRowSeparator(.hidden)
lengthSlider
.listRowSeparator(.hidden)
}
.padding(.horizontal)
.listRowBackground(Color(.systemBackground))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
private var advancedSettingsSection: some View {
Section(
header: Text(advancedSettingsTitle.uppercased())
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.secondary),
footer: VStack(alignment: .leading, spacing: 8) {
Text(dutyCycleHelperText)
Text(usageHoursHelperText)
}
.font(.caption)
.foregroundStyle(.secondary)
) {
dutyCycleSlider
.listRowSeparator(.hidden)
usageHoursSlider
.listRowSeparator(.hidden)
}
.listRowBackground(Color(.systemBackground))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
private var voltageSliderRange: ClosedRange<Double> {
@@ -788,6 +1038,14 @@ struct CalculatorView: View {
return 0...upperBound
}
private var dutyCycleRange: ClosedRange<Double> {
0...100
}
private var usageHoursRange: ClosedRange<Double> {
0...24
}
private var voltageSnapValues: [Double] {
[3.3, 5.0, 6.0, 9.0, 12.0, 13.8, 24.0, 28.0, 48.0]
}
@@ -920,6 +1178,52 @@ struct CalculatorView: View {
}
}
private var dutyCycleSlider: some View {
SliderSection(
title: dutyCycleTitle,
value: Binding(
get: { calculator.dutyCyclePercent },
set: { newValue in
let clamped = clampDutyCyclePercent(newValue)
calculator.dutyCyclePercent = clamped
}
),
range: dutyCycleRange,
unit: "%",
tapAction: beginDutyCycleEditing
)
.onChange(of: calculator.dutyCyclePercent) { _, newValue in
let clamped = clampDutyCyclePercent(newValue)
if abs(clamped - newValue) > 0.0001 {
calculator.dutyCyclePercent = clamped
}
autoUpdateSavedLoad()
}
}
private var usageHoursSlider: some View {
SliderSection(
title: usageHoursTitle,
value: Binding(
get: { calculator.dailyUsageHours },
set: { newValue in
let clamped = clampUsageHours(newValue)
calculator.dailyUsageHours = clamped
}
),
range: usageHoursRange,
unit: String(localized: "calculator.advanced.usage_hours.unit", comment: "Unit label for usage hours slider"),
tapAction: beginUsageHoursEditing
)
.onChange(of: calculator.dailyUsageHours) { _, newValue in
let clamped = clampUsageHours(newValue)
if abs(clamped - newValue) > 0.0001 {
calculator.dailyUsageHours = clamped
}
autoUpdateSavedLoad()
}
}
private func normalizedVoltage(for value: Double) -> Double {
let rounded = roundToTenth(value)
if let snap = nearestValue(to: rounded, in: voltageSnapValues, tolerance: 0.3) {
@@ -951,6 +1255,14 @@ struct CalculatorView: View {
return rounded
}
private func clampDutyCyclePercent(_ value: Double) -> Double {
min(100, max(0, roundToTenth(value)))
}
private func clampUsageHours(_ value: Double) -> Double {
min(24, max(0, roundToTenth(value)))
}
private func nearestValue(to value: Double, in options: [Double], tolerance: Double) -> Double? {
guard let closest = options.min(by: { abs($0 - value) < abs($1 - value) }) else { return nil }
return abs(closest - value) <= tolerance ? closest : nil
@@ -999,6 +1311,16 @@ struct CalculatorView: View {
lengthInput = formattedValue(calculator.length)
editingValue = .length
}
private func beginDutyCycleEditing() {
dutyCycleInput = formattedValue(calculator.dutyCyclePercent)
editingValue = .dutyCycle
}
private func beginUsageHoursEditing() {
usageHoursInput = formattedValue(calculator.dailyUsageHours)
editingValue = .usageHours
}
private func saveCurrentLoad() {
@@ -1013,6 +1335,8 @@ struct CalculatorView: View {
iconName: "lightbulb",
colorName: "blue",
isWattMode: isWattMode,
dutyCyclePercent: calculator.dutyCyclePercent,
dailyUsageHours: calculator.dailyUsageHours,
system: nil, // For now, new loads aren't associated with a system
remoteIconURLString: nil
)
@@ -1025,6 +1349,8 @@ struct CalculatorView: View {
calculator.current = savedLoad.current
calculator.power = savedLoad.power
calculator.length = savedLoad.length
calculator.dutyCyclePercent = savedLoad.dutyCyclePercent
calculator.dailyUsageHours = savedLoad.dailyUsageHours
isWattMode = savedLoad.isWattMode
completedItemIDs = Set(savedLoad.bomCompletedItemIDs)
}
@@ -1041,6 +1367,8 @@ struct CalculatorView: View {
savedLoad.crossSection = calculator.crossSection(for: .metric)
savedLoad.timestamp = Date()
savedLoad.isWattMode = isWattMode
savedLoad.dutyCyclePercent = calculator.dutyCyclePercent
savedLoad.dailyUsageHours = calculator.dailyUsageHours
savedLoad.bomCompletedItemIDs = Array(completedItemIDs).sorted()
// Icon and color are updated directly through bindings in the editor
}
@@ -1318,7 +1646,10 @@ struct LoadLibraryView: View {
calculator.loadName = savedLoad.name
calculator.voltage = savedLoad.voltage
calculator.current = savedLoad.current
calculator.power = savedLoad.power
calculator.length = savedLoad.length
calculator.dutyCyclePercent = savedLoad.dutyCyclePercent
calculator.dailyUsageHours = savedLoad.dailyUsageHours
}
private func deleteLoads(offsets: IndexSet) {

View File

@@ -301,45 +301,19 @@ struct SystemOverviewView: View {
} else {
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
batteryMetricsContent
}
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: 2),
alignment: .leading,
spacing: 16
) {
batteryMetricsContent
}
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.circle",
label: batteryEnergyLabel,
value: formattedValue(totalEnergy, unit: "Wh"),
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
batteryMetricsContent
}
}
}
@@ -355,6 +329,34 @@ struct SystemOverviewView: View {
.buttonStyle(.plain)
}
@ViewBuilder
private var batteryMetricsContent: some View {
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .blue
)
summaryMetric(
icon: "gauge.medium",
label: batteryCapacityLabel,
value: formattedValue(totalCapacity, unit: "Ah"),
tint: .orange
)
summaryMetric(
icon: "battery.100.bolt",
label: batteryUsableCapacityLabel,
value: formattedValue(totalUsableCapacity, unit: "Ah"),
tint: .teal
)
summaryMetric(
icon: "bolt.circle",
label: batteryUsableEnergyLabel,
value: formattedValue(totalUsableEnergy, unit: "Wh"),
tint: .green
)
}
private var chargersCard: some View {
Button(action: onSelectChargers) {
VStack(alignment: .leading, spacing: 16) {
@@ -467,6 +469,18 @@ struct SystemOverviewView: View {
}
}
private var totalUsableCapacity: Double {
batteries.reduce(0) { result, battery in
result + battery.usableCapacityAmpHours
}
}
private var totalUsableEnergy: Double {
batteries.reduce(0) { result, battery in
result + battery.usableEnergyWattHours
}
}
private var totalChargerCurrent: Double {
chargers.reduce(0) { result, charger in
result + max(0, charger.maxCurrentAmps)
@@ -611,8 +625,8 @@ struct SystemOverviewView: View {
}
private var estimatedRuntimeHours: Double? {
guard totalPower > 0, totalEnergy > 0 else { return nil }
let hours = totalEnergy / totalPower
guard totalPower > 0, totalUsableEnergy > 0 else { return nil }
let hours = totalUsableEnergy / totalPower
return hours.isFinite && hours > 0 ? hours : nil
}
@@ -738,12 +752,21 @@ struct SystemOverviewView: View {
)
}
private var batteryEnergyLabel: String {
private var batteryUsableCapacityLabel: String {
NSLocalizedString(
"battery.bank.metric.energy",
"battery.bank.metric.usable_capacity",
bundle: .main,
value: "Energy",
comment: "Label for total energy metric"
value: "Usable Capacity",
comment: "Label for usable capacity metric"
)
}
private var batteryUsableEnergyLabel: String {
NSLocalizedString(
"battery.bank.metric.usable_energy",
bundle: .main,
value: "Usable Energy",
comment: "Label for usable energy metric"
)
}

View File

@@ -7,6 +7,7 @@ class SavedBattery {
var name: String
var nominalVoltage: Double
var capacityAmpHours: Double
var usableCapacityOverrideFraction: Double?
private var chemistryRawValue: String
var iconName: String = "battery.100"
var colorName: String = "blue"
@@ -19,6 +20,7 @@ class SavedBattery {
nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100,
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
usableCapacityOverrideFraction: Double? = nil,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem? = nil,
@@ -28,6 +30,7 @@ class SavedBattery {
self.name = name
self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours
self.usableCapacityOverrideFraction = usableCapacityOverrideFraction
self.chemistryRawValue = chemistry.rawValue
self.iconName = iconName
self.colorName = colorName
@@ -47,4 +50,18 @@ class SavedBattery {
var energyWattHours: Double {
nominalVoltage * capacityAmpHours
}
var usableCapacityAmpHours: Double {
let fraction: Double
if let override = usableCapacityOverrideFraction {
fraction = max(0, min(1, override))
} else {
fraction = chemistry.usableCapacityFraction
}
return capacityAmpHours * fraction
}
var usableEnergyWattHours: Double {
usableCapacityAmpHours * nominalVoltage
}
}

View File

@@ -25,26 +25,18 @@ struct SettingsView: View {
.pickerStyle(.segmented)
}
Section {
HStack {
Text("Wire Cross-Section:")
Spacer()
Text(unitSettings.unitSystem.wireAreaUnit)
.foregroundColor(.secondary)
}
HStack {
Text("Length:")
Spacer()
Text(unitSettings.unitSystem.lengthUnit)
.foregroundColor(.secondary)
}
} header: {
Text("Current Units")
} footer: {
Text("Changing the unit system will apply to all calculations in the app.")
}
Section("Cable Pro") {
Toggle(isOn: $unitSettings.isProUnlocked) {
VStack(alignment: .leading, spacing: 4) {
Text("Early Access Features")
.font(.body.weight(.semibold))
Text("Enable experimental tools that will require a paid upgrade later on.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
Section {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
@@ -88,4 +80,10 @@ struct SettingsView: View {
}
}
}
}
}
#Preview("Settings (Default)") {
let settings = UnitSystemSettings()
return SettingsView()
.environmentObject(settings)
}

View File

@@ -46,9 +46,15 @@ class UnitSystemSettings: ObservableObject {
UserDefaults.standard.set(unitSystem.rawValue, forKey: "unitSystem")
}
}
@Published var isProUnlocked: Bool {
didSet {
UserDefaults.standard.set(isProUnlocked, forKey: "isProUnlocked")
}
}
init() {
let savedSystem = UserDefaults.standard.string(forKey: "unitSystem") ?? UnitSystem.metric.rawValue
self.unitSystem = UnitSystem(rawValue: savedSystem) ?? .metric
self.isProUnlocked = UserDefaults.standard.bool(forKey: "isProUnlocked")
}
}

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Kabellänge (%@)";
"slider.power.title" = "Leistung";
"slider.voltage.title" = "Spannung";
"calculator.advanced.section.title" = "Erweitert";
"calculator.advanced.duty_cycle.title" = "Einschaltdauer";
"calculator.advanced.duty_cycle.helper" = "Prozentsatz der aktiven Zeit, in der die Last tatsächlich Leistung aufnimmt.";
"calculator.advanced.usage_hours.title" = "Tägliche Laufzeit";
"calculator.advanced.usage_hours.helper" = "Stunden pro Tag, in denen die Last eingeschaltet ist.";
"calculator.advanced.usage_hours.unit" = "h/Tag";
"calculator.alert.duty_cycle.title" = "Einschaltdauer bearbeiten";
"calculator.alert.duty_cycle.placeholder" = "Einschaltdauer";
"calculator.alert.duty_cycle.message" = "Einschaltdauer als Prozent (0-100 %) eingeben.";
"calculator.alert.usage_hours.title" = "Tägliche Laufzeit bearbeiten";
"calculator.alert.usage_hours.placeholder" = "Tägliche Laufzeit";
"calculator.alert.usage_hours.message" = "Stunden pro Tag eingeben, in denen die Last aktiv ist.";
"system.list.no.components" = "Noch keine Verbraucher";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Metrisch (mm², m)";
@@ -172,6 +184,8 @@
"battery.bank.metric.count" = "Batterien";
"battery.bank.metric.capacity" = "Kapazität";
"battery.bank.metric.energy" = "Energie";
"battery.bank.metric.usable_capacity" = "Nutzbare Kapazität";
"battery.bank.metric.usable_energy" = "Nutzbare Energie";
"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.";
@@ -203,12 +217,20 @@
"battery.editor.section.summary" = "Übersicht";
"battery.editor.slider.voltage" = "Nennspannung";
"battery.editor.slider.capacity" = "Kapazität";
"battery.editor.slider.usable_capacity" = "Nutzbare Kapazität (%)";
"battery.editor.section.advanced" = "Erweitert";
"battery.editor.button.reset_default" = "Zurücksetzen";
"battery.editor.advanced.usable_capacity.footer_default" = "Standardwert %@ basierend auf der Chemie.";
"battery.editor.advanced.usable_capacity.footer_override" = "Überschreibung aktiv. Chemie-Standard bleibt %@.";
"battery.editor.alert.voltage.title" = "Nennspannung bearbeiten";
"battery.editor.alert.voltage.placeholder" = "Spannung";
"battery.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
"battery.editor.alert.capacity.title" = "Kapazität bearbeiten";
"battery.editor.alert.capacity.placeholder" = "Kapazität";
"battery.editor.alert.capacity.message" = "Kapazität in Amperestunden (Ah) eingeben";
"battery.editor.alert.usable_capacity.title" = "Nutzbare Kapazität bearbeiten";
"battery.editor.alert.usable_capacity.placeholder" = "Nutzbare Kapazität (%)";
"battery.editor.alert.usable_capacity.message" = "Nutzbare Kapazität in Prozent (%) eingeben";
"battery.editor.alert.cancel" = "Abbrechen";
"battery.editor.alert.save" = "Speichern";
"battery.editor.default_name" = "Neue Batterie";

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Longitud del cable (%@)";
"slider.power.title" = "Potencia";
"slider.voltage.title" = "Voltaje";
"calculator.advanced.section.title" = "Configuración avanzada";
"calculator.advanced.duty_cycle.title" = "Ciclo de trabajo";
"calculator.advanced.duty_cycle.helper" = "Porcentaje del tiempo activo en el que la carga consume energía.";
"calculator.advanced.usage_hours.title" = "Tiempo encendido diario";
"calculator.advanced.usage_hours.helper" = "Horas por día que la carga permanece encendida.";
"calculator.advanced.usage_hours.unit" = "h/día";
"calculator.alert.duty_cycle.title" = "Editar ciclo de trabajo";
"calculator.alert.duty_cycle.placeholder" = "Ciclo de trabajo";
"calculator.alert.duty_cycle.message" = "Introduce el porcentaje de ciclo de trabajo (0-100%).";
"calculator.alert.usage_hours.title" = "Editar tiempo encendido diario";
"calculator.alert.usage_hours.placeholder" = "Tiempo encendido diario";
"calculator.alert.usage_hours.message" = "Introduce las horas por día que la carga está activa.";
"system.list.no.components" = "Aún no hay componentes";
"units.imperial.display" = "Imperial (AWG, ft)";
"units.metric.display" = "Métrico (mm², m)";
@@ -171,6 +183,8 @@
"battery.bank.metric.count" = "Baterías";
"battery.bank.metric.capacity" = "Capacidad";
"battery.bank.metric.energy" = "Energía";
"battery.bank.metric.usable_capacity" = "Capacidad utilizable";
"battery.bank.metric.usable_energy" = "Energía utilizable";
"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.";
@@ -202,12 +216,20 @@
"battery.editor.section.summary" = "Resumen";
"battery.editor.slider.voltage" = "Voltaje nominal";
"battery.editor.slider.capacity" = "Capacidad";
"battery.editor.slider.usable_capacity" = "Capacidad utilizable (%)";
"battery.editor.section.advanced" = "Avanzado";
"battery.editor.button.reset_default" = "Restablecer";
"battery.editor.advanced.usable_capacity.footer_default" = "Valor predeterminado %@ basado en la química.";
"battery.editor.advanced.usable_capacity.footer_override" = "Sobrescritura activa. El valor predeterminado por química sigue siendo %@.";
"battery.editor.alert.voltage.title" = "Editar voltaje nominal";
"battery.editor.alert.voltage.placeholder" = "Voltaje";
"battery.editor.alert.voltage.message" = "Introduce el voltaje en voltios (V)";
"battery.editor.alert.capacity.title" = "Editar capacidad";
"battery.editor.alert.capacity.placeholder" = "Capacidad";
"battery.editor.alert.capacity.message" = "Introduce la capacidad en amperios-hora (Ah)";
"battery.editor.alert.usable_capacity.title" = "Editar capacidad utilizable";
"battery.editor.alert.usable_capacity.placeholder" = "Capacidad utilizable (%)";
"battery.editor.alert.usable_capacity.message" = "Introduce el porcentaje de capacidad utilizable (%)";
"battery.editor.alert.cancel" = "Cancelar";
"battery.editor.alert.save" = "Guardar";
"battery.editor.default_name" = "Nueva batería";

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Longueur du câble (%@)";
"slider.power.title" = "Puissance";
"slider.voltage.title" = "Tension";
"calculator.advanced.section.title" = "Paramètres avancés";
"calculator.advanced.duty_cycle.title" = "Facteur de marche";
"calculator.advanced.duty_cycle.helper" = "Pourcentage du temps actif pendant lequel la charge consomme réellement de l'énergie.";
"calculator.advanced.usage_hours.title" = "Temps de fonctionnement quotidien";
"calculator.advanced.usage_hours.helper" = "Heures par jour pendant lesquelles la charge est allumée.";
"calculator.advanced.usage_hours.unit" = "h/jour";
"calculator.alert.duty_cycle.title" = "Modifier le facteur de marche";
"calculator.alert.duty_cycle.placeholder" = "Facteur de marche";
"calculator.alert.duty_cycle.message" = "Saisissez le facteur de marche en pourcentage (0-100 %).";
"calculator.alert.usage_hours.title" = "Modifier le temps de fonctionnement quotidien";
"calculator.alert.usage_hours.placeholder" = "Temps de fonctionnement quotidien";
"calculator.alert.usage_hours.message" = "Saisissez le nombre d'heures par jour pendant lesquelles la charge est active.";
"system.list.no.components" = "Aucun composant pour l'instant";
"units.imperial.display" = "Impérial (AWG, ft)";
"units.metric.display" = "Métrique (mm², m)";
@@ -171,6 +183,8 @@
"battery.bank.metric.count" = "Batteries";
"battery.bank.metric.capacity" = "Capacité";
"battery.bank.metric.energy" = "Énergie";
"battery.bank.metric.usable_capacity" = "Capacité utilisable";
"battery.bank.metric.usable_energy" = "Énergie utilisable";
"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.";
@@ -202,12 +216,20 @@
"battery.editor.section.summary" = "Résumé";
"battery.editor.slider.voltage" = "Tension nominale";
"battery.editor.slider.capacity" = "Capacité";
"battery.editor.slider.usable_capacity" = "Capacité utilisable (%)";
"battery.editor.section.advanced" = "Avancé";
"battery.editor.button.reset_default" = "Réinitialiser";
"battery.editor.advanced.usable_capacity.footer_default" = "Valeur par défaut %@ selon la chimie.";
"battery.editor.advanced.usable_capacity.footer_override" = "Remplacement actif. La valeur par défaut liée à la chimie reste %@.";
"battery.editor.alert.voltage.title" = "Modifier la tension nominale";
"battery.editor.alert.voltage.placeholder" = "Tension";
"battery.editor.alert.voltage.message" = "Saisissez la tension en volts (V)";
"battery.editor.alert.capacity.title" = "Modifier la capacité";
"battery.editor.alert.capacity.placeholder" = "Capacité";
"battery.editor.alert.capacity.message" = "Saisissez la capacité en ampères-heures (Ah)";
"battery.editor.alert.usable_capacity.title" = "Modifier la capacité utilisable";
"battery.editor.alert.usable_capacity.placeholder" = "Capacité utilisable (%)";
"battery.editor.alert.usable_capacity.message" = "Saisissez le pourcentage de capacité utilisable (%)";
"battery.editor.alert.cancel" = "Annuler";
"battery.editor.alert.save" = "Enregistrer";
"battery.editor.default_name" = "Nouvelle batterie";

View File

@@ -33,6 +33,18 @@
"slider.length.title" = "Kabellengte (%@)";
"slider.power.title" = "Vermogen";
"slider.voltage.title" = "Spanning";
"calculator.advanced.section.title" = "Geavanceerde instellingen";
"calculator.advanced.duty_cycle.title" = "Inschakelduur";
"calculator.advanced.duty_cycle.helper" = "Percentage van de actieve tijd waarin de belasting daadwerkelijk vermogen vraagt.";
"calculator.advanced.usage_hours.title" = "Dagelijkse aan-tijd";
"calculator.advanced.usage_hours.helper" = "Uren per dag dat de belasting is ingeschakeld.";
"calculator.advanced.usage_hours.unit" = "u/dag";
"calculator.alert.duty_cycle.title" = "Inschakelduur bewerken";
"calculator.alert.duty_cycle.placeholder" = "Inschakelduur";
"calculator.alert.duty_cycle.message" = "Voer de inschakelduur in als percentage (0-100%).";
"calculator.alert.usage_hours.title" = "Dagelijkse aan-tijd bewerken";
"calculator.alert.usage_hours.placeholder" = "Dagelijkse aan-tijd";
"calculator.alert.usage_hours.message" = "Voer het aantal uren per dag in dat de belasting actief is.";
"system.list.no.components" = "Nog geen componenten";
"units.imperial.display" = "Imperiaal (AWG, ft)";
"units.metric.display" = "Metrisch (mm², m)";
@@ -171,6 +183,8 @@
"battery.bank.metric.count" = "Batterijen";
"battery.bank.metric.capacity" = "Capaciteit";
"battery.bank.metric.energy" = "Energie";
"battery.bank.metric.usable_capacity" = "Beschikbare capaciteit";
"battery.bank.metric.usable_energy" = "Beschikbare energie";
"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.";
@@ -202,12 +216,20 @@
"battery.editor.section.summary" = "Overzicht";
"battery.editor.slider.voltage" = "Nominale spanning";
"battery.editor.slider.capacity" = "Capaciteit";
"battery.editor.slider.usable_capacity" = "Beschikbare capaciteit (%)";
"battery.editor.section.advanced" = "Geavanceerd";
"battery.editor.button.reset_default" = "Resetten";
"battery.editor.advanced.usable_capacity.footer_default" = "Standaardwaarde %@ op basis van de chemie.";
"battery.editor.advanced.usable_capacity.footer_override" = "Handmatige override actief. Chemische standaard blijft %@.";
"battery.editor.alert.voltage.title" = "Nominale spanning bewerken";
"battery.editor.alert.voltage.placeholder" = "Spanning";
"battery.editor.alert.voltage.message" = "Voer de spanning in volt (V) in";
"battery.editor.alert.capacity.title" = "Capaciteit bewerken";
"battery.editor.alert.capacity.placeholder" = "Capaciteit";
"battery.editor.alert.capacity.message" = "Voer de capaciteit in ampère-uur (Ah) in";
"battery.editor.alert.usable_capacity.title" = "Beschikbare capaciteit bewerken";
"battery.editor.alert.usable_capacity.placeholder" = "Beschikbare capaciteit (%)";
"battery.editor.alert.usable_capacity.message" = "Voer het percentage beschikbare capaciteit (%) in";
"battery.editor.alert.cancel" = "Annuleren";
"battery.editor.alert.save" = "Opslaan";
"battery.editor.default_name" = "Nieuwe batterij";