some advanced settings
This commit is contained in:
@@ -32,6 +32,18 @@
|
|||||||
"slider.length.title" = "Cable Length (%@)";
|
"slider.length.title" = "Cable Length (%@)";
|
||||||
"slider.power.title" = "Power";
|
"slider.power.title" = "Power";
|
||||||
"slider.voltage.title" = "Voltage";
|
"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";
|
"system.list.no.components" = "No components yet";
|
||||||
"units.imperial.display" = "Imperial (AWG, ft)";
|
"units.imperial.display" = "Imperial (AWG, ft)";
|
||||||
"units.metric.display" = "Metric (mm², m)";
|
"units.metric.display" = "Metric (mm², m)";
|
||||||
@@ -104,6 +116,8 @@
|
|||||||
"battery.bank.metric.count" = "Batteries";
|
"battery.bank.metric.count" = "Batteries";
|
||||||
"battery.bank.metric.capacity" = "Capacity";
|
"battery.bank.metric.capacity" = "Capacity";
|
||||||
"battery.bank.metric.energy" = "Energy";
|
"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.overview.empty.create" = "Add Battery";
|
||||||
"battery.onboarding.title" = "Add your first 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.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.section.summary" = "Summary";
|
||||||
"battery.editor.slider.voltage" = "Nominal Voltage";
|
"battery.editor.slider.voltage" = "Nominal Voltage";
|
||||||
"battery.editor.slider.capacity" = "Capacity";
|
"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.title" = "Edit Nominal Voltage";
|
||||||
"battery.editor.alert.voltage.placeholder" = "Voltage";
|
"battery.editor.alert.voltage.placeholder" = "Voltage";
|
||||||
"battery.editor.alert.voltage.message" = "Enter voltage in volts (V)";
|
"battery.editor.alert.voltage.message" = "Enter voltage in volts (V)";
|
||||||
"battery.editor.alert.capacity.title" = "Edit Capacity";
|
"battery.editor.alert.capacity.title" = "Edit Capacity";
|
||||||
"battery.editor.alert.capacity.placeholder" = "Capacity";
|
"battery.editor.alert.capacity.placeholder" = "Capacity";
|
||||||
"battery.editor.alert.capacity.message" = "Enter capacity in amp-hours (Ah)";
|
"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.cancel" = "Cancel";
|
||||||
"battery.editor.alert.save" = "Save";
|
"battery.editor.alert.save" = "Save";
|
||||||
"battery.editor.default_name" = "New Battery";
|
"battery.editor.default_name" = "New Battery";
|
||||||
|
|||||||
@@ -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 {
|
private var badgeVoltageLabel: String {
|
||||||
String(
|
String(
|
||||||
localized: "battery.bank.badge.voltage",
|
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 {
|
private var emptyTitle: String {
|
||||||
String(
|
String(
|
||||||
localized: "battery.bank.empty.title",
|
localized: "battery.bank.empty.title",
|
||||||
@@ -190,49 +206,15 @@ struct BatteriesView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewThatFits(in: .horizontal) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
summaryMetric(
|
ForEach(Array(summaryMetrics.enumerated()), id: \.offset) { _, metric in
|
||||||
icon: "battery.100",
|
summaryMetric(icon: metric.icon, label: metric.label, value: metric.value, tint: metric.tint)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 2)
|
||||||
}
|
}
|
||||||
|
.scrollClipDisabled(false)
|
||||||
|
|
||||||
if let status = bankStatus {
|
if let status = bankStatus {
|
||||||
Button {
|
Button {
|
||||||
@@ -285,24 +267,7 @@ struct BatteriesView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
batteryMetricsScroll(for: battery)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 16)
|
.padding(.vertical, 16)
|
||||||
.padding(.horizontal, 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 {
|
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
|
||||||
ComponentSummaryMetricView(
|
ComponentSummaryMetricView(
|
||||||
icon: icon,
|
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 {
|
private func metricBadge(label: String, value: String, tint: Color) -> some View {
|
||||||
ComponentMetricBadgeView(
|
ComponentMetricBadgeView(
|
||||||
label: label,
|
label: label,
|
||||||
@@ -401,6 +436,13 @@ struct BatteriesView: View {
|
|||||||
return "\(numberString) \(unit)"
|
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? {
|
private var dominantVoltage: Double? {
|
||||||
guard batteries.count > 1 else { return nil }
|
guard batteries.count > 1 else { return nil }
|
||||||
return dominantValue(
|
return dominantValue(
|
||||||
|
|||||||
@@ -14,12 +14,28 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
|||||||
var displayName: String {
|
var displayName: String {
|
||||||
rawValue
|
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
|
let id: UUID
|
||||||
var name: String
|
var name: String
|
||||||
var nominalVoltage: Double
|
var nominalVoltage: Double
|
||||||
var capacityAmpHours: Double
|
var capacityAmpHours: Double
|
||||||
|
var usableCapacityOverrideFraction: Double?
|
||||||
var chemistry: Chemistry
|
var chemistry: Chemistry
|
||||||
var iconName: String
|
var iconName: String
|
||||||
var colorName: String
|
var colorName: String
|
||||||
@@ -31,6 +47,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
|||||||
nominalVoltage: Double = 12.8,
|
nominalVoltage: Double = 12.8,
|
||||||
capacityAmpHours: Double = 100,
|
capacityAmpHours: Double = 100,
|
||||||
chemistry: Chemistry = .lithiumIronPhosphate,
|
chemistry: Chemistry = .lithiumIronPhosphate,
|
||||||
|
usableCapacityOverrideFraction: Double? = nil,
|
||||||
iconName: String = "battery.100",
|
iconName: String = "battery.100",
|
||||||
colorName: String = "blue",
|
colorName: String = "blue",
|
||||||
system: ElectricalSystem
|
system: ElectricalSystem
|
||||||
@@ -39,6 +56,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.nominalVoltage = nominalVoltage
|
self.nominalVoltage = nominalVoltage
|
||||||
self.capacityAmpHours = capacityAmpHours
|
self.capacityAmpHours = capacityAmpHours
|
||||||
|
self.usableCapacityOverrideFraction = usableCapacityOverrideFraction
|
||||||
self.chemistry = chemistry
|
self.chemistry = chemistry
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.colorName = colorName
|
self.colorName = colorName
|
||||||
@@ -50,6 +68,7 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
|||||||
self.name = savedBattery.name
|
self.name = savedBattery.name
|
||||||
self.nominalVoltage = savedBattery.nominalVoltage
|
self.nominalVoltage = savedBattery.nominalVoltage
|
||||||
self.capacityAmpHours = savedBattery.capacityAmpHours
|
self.capacityAmpHours = savedBattery.capacityAmpHours
|
||||||
|
self.usableCapacityOverrideFraction = savedBattery.usableCapacityOverrideFraction
|
||||||
self.chemistry = savedBattery.chemistry
|
self.chemistry = savedBattery.chemistry
|
||||||
self.iconName = savedBattery.iconName
|
self.iconName = savedBattery.iconName
|
||||||
self.colorName = savedBattery.colorName
|
self.colorName = savedBattery.colorName
|
||||||
@@ -60,10 +79,34 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
|||||||
nominalVoltage * capacityAmpHours
|
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) {
|
func apply(to savedBattery: SavedBattery) {
|
||||||
savedBattery.name = name
|
savedBattery.name = name
|
||||||
savedBattery.nominalVoltage = nominalVoltage
|
savedBattery.nominalVoltage = nominalVoltage
|
||||||
savedBattery.capacityAmpHours = capacityAmpHours
|
savedBattery.capacityAmpHours = capacityAmpHours
|
||||||
|
savedBattery.usableCapacityOverrideFraction = usableCapacityOverrideFraction
|
||||||
savedBattery.chemistry = chemistry
|
savedBattery.chemistry = chemistry
|
||||||
savedBattery.iconName = iconName
|
savedBattery.iconName = iconName
|
||||||
savedBattery.colorName = colorName
|
savedBattery.colorName = colorName
|
||||||
@@ -78,6 +121,7 @@ extension BatteryConfiguration {
|
|||||||
lhs.name == rhs.name &&
|
lhs.name == rhs.name &&
|
||||||
lhs.nominalVoltage == rhs.nominalVoltage &&
|
lhs.nominalVoltage == rhs.nominalVoltage &&
|
||||||
lhs.capacityAmpHours == rhs.capacityAmpHours &&
|
lhs.capacityAmpHours == rhs.capacityAmpHours &&
|
||||||
|
lhs.usableCapacityOverrideFraction == rhs.usableCapacityOverrideFraction &&
|
||||||
lhs.chemistry == rhs.chemistry &&
|
lhs.chemistry == rhs.chemistry &&
|
||||||
lhs.iconName == rhs.iconName &&
|
lhs.iconName == rhs.iconName &&
|
||||||
lhs.colorName == rhs.colorName
|
lhs.colorName == rhs.colorName
|
||||||
@@ -88,6 +132,7 @@ extension BatteryConfiguration {
|
|||||||
hasher.combine(name)
|
hasher.combine(name)
|
||||||
hasher.combine(nominalVoltage)
|
hasher.combine(nominalVoltage)
|
||||||
hasher.combine(capacityAmpHours)
|
hasher.combine(capacityAmpHours)
|
||||||
|
hasher.combine(usableCapacityOverrideFraction)
|
||||||
hasher.combine(chemistry)
|
hasher.combine(chemistry)
|
||||||
hasher.combine(iconName)
|
hasher.combine(iconName)
|
||||||
hasher.combine(colorName)
|
hasher.combine(colorName)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class CableCalculator: ObservableObject {
|
|||||||
@Published var power: Double = 60.0
|
@Published var power: Double = 60.0
|
||||||
@Published var length: Double = 10.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 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 {
|
var calculatedPower: Double {
|
||||||
voltage * current
|
voltage * current
|
||||||
@@ -132,6 +134,8 @@ class SavedLoad {
|
|||||||
var iconName: String = "lightbulb"
|
var iconName: String = "lightbulb"
|
||||||
var colorName: String = "blue"
|
var colorName: String = "blue"
|
||||||
var isWattMode: Bool = false
|
var isWattMode: Bool = false
|
||||||
|
var dutyCyclePercent: Double = 100.0
|
||||||
|
var dailyUsageHours: Double = 1.0
|
||||||
var system: ElectricalSystem?
|
var system: ElectricalSystem?
|
||||||
var remoteIconURLString: String? = nil
|
var remoteIconURLString: String? = nil
|
||||||
var affiliateURLString: String? = nil
|
var affiliateURLString: String? = nil
|
||||||
@@ -139,7 +143,25 @@ class SavedLoad {
|
|||||||
var bomCompletedItemIDs: [String] = []
|
var bomCompletedItemIDs: [String] = []
|
||||||
var identifier: String = UUID().uuidString
|
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.name = name
|
||||||
self.voltage = voltage
|
self.voltage = voltage
|
||||||
self.current = current
|
self.current = current
|
||||||
@@ -150,6 +172,8 @@ class SavedLoad {
|
|||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.colorName = colorName
|
self.colorName = colorName
|
||||||
self.isWattMode = isWattMode
|
self.isWattMode = isWattMode
|
||||||
|
self.dutyCyclePercent = dutyCyclePercent
|
||||||
|
self.dailyUsageHours = dailyUsageHours
|
||||||
self.system = system
|
self.system = system
|
||||||
self.remoteIconURLString = remoteIconURLString
|
self.remoteIconURLString = remoteIconURLString
|
||||||
self.affiliateURLString = affiliateURLString
|
self.affiliateURLString = affiliateURLString
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ struct CalculatorView: View {
|
|||||||
@State private var currentInput: String = ""
|
@State private var currentInput: String = ""
|
||||||
@State private var powerInput: String = ""
|
@State private var powerInput: String = ""
|
||||||
@State private var lengthInput: String = ""
|
@State private var lengthInput: String = ""
|
||||||
|
@State private var dutyCycleInput: String = ""
|
||||||
|
@State private var usageHoursInput: String = ""
|
||||||
@State private var showingLoadEditor = false
|
@State private var showingLoadEditor = false
|
||||||
@State private var presentedAffiliateLink: AffiliateLinkInfo?
|
@State private var presentedAffiliateLink: AffiliateLinkInfo?
|
||||||
@State private var completedItemIDs: Set<String>
|
@State private var completedItemIDs: Set<String>
|
||||||
@@ -34,7 +36,7 @@ struct CalculatorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum EditingValue {
|
enum EditingValue {
|
||||||
case voltage, current, power, length
|
case voltage, current, power, length, dutyCycle, usageHours
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let editFormatter: NumberFormatter = {
|
private static let editFormatter: NumberFormatter = {
|
||||||
@@ -69,72 +71,24 @@ struct CalculatorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
attachAlerts(
|
||||||
badgesSection
|
attachSheets(
|
||||||
circuitDiagram
|
navigationWrapped(mainLayout)
|
||||||
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()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
.sheet(item: $presentedAffiliateLink) { info in
|
}
|
||||||
BillOfMaterialsView(
|
|
||||||
info: info,
|
private func attachAlerts<V: View>(_ view: V) -> some View {
|
||||||
items: buildBillOfMaterialsItems(deviceLink: info.affiliateURL),
|
let withLength = addLengthAlert(to: view)
|
||||||
completedItemIDs: $completedItemIDs
|
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(
|
.alert("Edit Length", isPresented: Binding(
|
||||||
get: { editingValue == .length },
|
get: { editingValue == .length },
|
||||||
set: { isPresented in
|
set: { isPresented in
|
||||||
@@ -151,10 +105,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 = ""
|
||||||
@@ -171,6 +125,10 @@ struct CalculatorView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Enter length in \(unitSettings.unitSystem.lengthUnit)")
|
Text("Enter length in \(unitSettings.unitSystem.lengthUnit)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addVoltageAlert<V: View>(to view: V) -> some View {
|
||||||
|
view
|
||||||
.alert("Edit Voltage", isPresented: Binding(
|
.alert("Edit Voltage", isPresented: Binding(
|
||||||
get: { editingValue == .voltage },
|
get: { editingValue == .voltage },
|
||||||
set: { isPresented in
|
set: { isPresented in
|
||||||
@@ -187,10 +145,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 = ""
|
||||||
@@ -212,6 +170,10 @@ struct CalculatorView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Enter voltage in volts (V)")
|
Text("Enter voltage in volts (V)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addCurrentAlert<V: View>(to view: V) -> some View {
|
||||||
|
view
|
||||||
.alert("Edit Current", isPresented: Binding(
|
.alert("Edit Current", isPresented: Binding(
|
||||||
get: { editingValue == .current },
|
get: { editingValue == .current },
|
||||||
set: { isPresented in
|
set: { isPresented in
|
||||||
@@ -228,10 +190,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 = ""
|
||||||
@@ -249,6 +211,10 @@ struct CalculatorView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Enter current in amperes (A)")
|
Text("Enter current in amperes (A)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addPowerAlert<V: View>(to view: V) -> some View {
|
||||||
|
view
|
||||||
.alert("Edit Power", isPresented: Binding(
|
.alert("Edit Power", isPresented: Binding(
|
||||||
get: { editingValue == .power },
|
get: { editingValue == .power },
|
||||||
set: { isPresented in
|
set: { isPresented in
|
||||||
@@ -265,10 +231,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 = ""
|
||||||
@@ -286,14 +252,189 @@ struct CalculatorView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text("Enter power in watts (W)")
|
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 {
|
private var loadIcon: String {
|
||||||
@@ -596,15 +737,20 @@ struct CalculatorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var mainContent: some View {
|
private var mainContent: some View {
|
||||||
ScrollView {
|
List {
|
||||||
VStack(spacing: 20) {
|
slidersSection
|
||||||
Divider().padding(.horizontal)
|
advancedSettingsSection
|
||||||
slidersSection
|
if let info = affiliateLinkInfo {
|
||||||
if let info = affiliateLinkInfo {
|
affiliateLinkSection(info: info)
|
||||||
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
|
@ViewBuilder
|
||||||
@@ -640,7 +786,6 @@ struct CalculatorView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.background(Color(.secondarySystemBackground))
|
.background(Color(.secondarySystemBackground))
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||||
.padding(.horizontal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildBillOfMaterialsItems(deviceLink: URL?) -> [BOMItem] {
|
private func buildBillOfMaterialsItems(deviceLink: URL?) -> [BOMItem] {
|
||||||
@@ -758,13 +903,118 @@ struct CalculatorView: View {
|
|||||||
return items
|
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 {
|
private var slidersSection: some View {
|
||||||
VStack(spacing: 30) {
|
Section {
|
||||||
voltageSlider
|
voltageSlider
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
currentPowerSlider
|
currentPowerSlider
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
lengthSlider
|
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> {
|
private var voltageSliderRange: ClosedRange<Double> {
|
||||||
@@ -788,6 +1038,14 @@ struct CalculatorView: View {
|
|||||||
return 0...upperBound
|
return 0...upperBound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var dutyCycleRange: ClosedRange<Double> {
|
||||||
|
0...100
|
||||||
|
}
|
||||||
|
|
||||||
|
private var usageHoursRange: ClosedRange<Double> {
|
||||||
|
0...24
|
||||||
|
}
|
||||||
|
|
||||||
private var voltageSnapValues: [Double] {
|
private var voltageSnapValues: [Double] {
|
||||||
[3.3, 5.0, 6.0, 9.0, 12.0, 13.8, 24.0, 28.0, 48.0]
|
[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 {
|
private func normalizedVoltage(for value: Double) -> Double {
|
||||||
let rounded = roundToTenth(value)
|
let rounded = roundToTenth(value)
|
||||||
if let snap = nearestValue(to: rounded, in: voltageSnapValues, tolerance: 0.3) {
|
if let snap = nearestValue(to: rounded, in: voltageSnapValues, tolerance: 0.3) {
|
||||||
@@ -951,6 +1255,14 @@ struct CalculatorView: View {
|
|||||||
return rounded
|
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? {
|
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 }
|
guard let closest = options.min(by: { abs($0 - value) < abs($1 - value) }) else { return nil }
|
||||||
return abs(closest - value) <= tolerance ? closest : nil
|
return abs(closest - value) <= tolerance ? closest : nil
|
||||||
@@ -999,6 +1311,16 @@ struct CalculatorView: View {
|
|||||||
lengthInput = formattedValue(calculator.length)
|
lengthInput = formattedValue(calculator.length)
|
||||||
editingValue = .length
|
editingValue = .length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func beginDutyCycleEditing() {
|
||||||
|
dutyCycleInput = formattedValue(calculator.dutyCyclePercent)
|
||||||
|
editingValue = .dutyCycle
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beginUsageHoursEditing() {
|
||||||
|
usageHoursInput = formattedValue(calculator.dailyUsageHours)
|
||||||
|
editingValue = .usageHours
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func saveCurrentLoad() {
|
private func saveCurrentLoad() {
|
||||||
@@ -1013,6 +1335,8 @@ struct CalculatorView: View {
|
|||||||
iconName: "lightbulb",
|
iconName: "lightbulb",
|
||||||
colorName: "blue",
|
colorName: "blue",
|
||||||
isWattMode: isWattMode,
|
isWattMode: isWattMode,
|
||||||
|
dutyCyclePercent: calculator.dutyCyclePercent,
|
||||||
|
dailyUsageHours: calculator.dailyUsageHours,
|
||||||
system: nil, // For now, new loads aren't associated with a system
|
system: nil, // For now, new loads aren't associated with a system
|
||||||
remoteIconURLString: nil
|
remoteIconURLString: nil
|
||||||
)
|
)
|
||||||
@@ -1025,6 +1349,8 @@ struct CalculatorView: View {
|
|||||||
calculator.current = savedLoad.current
|
calculator.current = savedLoad.current
|
||||||
calculator.power = savedLoad.power
|
calculator.power = savedLoad.power
|
||||||
calculator.length = savedLoad.length
|
calculator.length = savedLoad.length
|
||||||
|
calculator.dutyCyclePercent = savedLoad.dutyCyclePercent
|
||||||
|
calculator.dailyUsageHours = savedLoad.dailyUsageHours
|
||||||
isWattMode = savedLoad.isWattMode
|
isWattMode = savedLoad.isWattMode
|
||||||
completedItemIDs = Set(savedLoad.bomCompletedItemIDs)
|
completedItemIDs = Set(savedLoad.bomCompletedItemIDs)
|
||||||
}
|
}
|
||||||
@@ -1041,6 +1367,8 @@ struct CalculatorView: View {
|
|||||||
savedLoad.crossSection = calculator.crossSection(for: .metric)
|
savedLoad.crossSection = calculator.crossSection(for: .metric)
|
||||||
savedLoad.timestamp = Date()
|
savedLoad.timestamp = Date()
|
||||||
savedLoad.isWattMode = isWattMode
|
savedLoad.isWattMode = isWattMode
|
||||||
|
savedLoad.dutyCyclePercent = calculator.dutyCyclePercent
|
||||||
|
savedLoad.dailyUsageHours = calculator.dailyUsageHours
|
||||||
savedLoad.bomCompletedItemIDs = Array(completedItemIDs).sorted()
|
savedLoad.bomCompletedItemIDs = Array(completedItemIDs).sorted()
|
||||||
// Icon and color are updated directly through bindings in the editor
|
// Icon and color are updated directly through bindings in the editor
|
||||||
}
|
}
|
||||||
@@ -1318,7 +1646,10 @@ struct LoadLibraryView: View {
|
|||||||
calculator.loadName = savedLoad.name
|
calculator.loadName = savedLoad.name
|
||||||
calculator.voltage = savedLoad.voltage
|
calculator.voltage = savedLoad.voltage
|
||||||
calculator.current = savedLoad.current
|
calculator.current = savedLoad.current
|
||||||
|
calculator.power = savedLoad.power
|
||||||
calculator.length = savedLoad.length
|
calculator.length = savedLoad.length
|
||||||
|
calculator.dutyCyclePercent = savedLoad.dutyCyclePercent
|
||||||
|
calculator.dailyUsageHours = savedLoad.dailyUsageHours
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteLoads(offsets: IndexSet) {
|
private func deleteLoads(offsets: IndexSet) {
|
||||||
|
|||||||
@@ -301,45 +301,19 @@ struct SystemOverviewView: View {
|
|||||||
} else {
|
} else {
|
||||||
ViewThatFits(in: .horizontal) {
|
ViewThatFits(in: .horizontal) {
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
summaryMetric(
|
batteryMetricsContent
|
||||||
icon: "battery.100",
|
}
|
||||||
label: batteryCountLabel,
|
|
||||||
value: "\(batteries.count)",
|
LazyVGrid(
|
||||||
tint: .blue
|
columns: Array(repeating: GridItem(.flexible(), spacing: 16), count: 2),
|
||||||
).frame(maxWidth: .infinity, alignment: .leading)
|
alignment: .leading,
|
||||||
summaryMetric(
|
spacing: 16
|
||||||
icon: "gauge.medium",
|
) {
|
||||||
label: batteryCapacityLabel,
|
batteryMetricsContent
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
summaryMetric(
|
batteryMetricsContent
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,6 +329,34 @@ struct SystemOverviewView: View {
|
|||||||
.buttonStyle(.plain)
|
.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 {
|
private var chargersCard: some View {
|
||||||
Button(action: onSelectChargers) {
|
Button(action: onSelectChargers) {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
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 {
|
private var totalChargerCurrent: Double {
|
||||||
chargers.reduce(0) { result, charger in
|
chargers.reduce(0) { result, charger in
|
||||||
result + max(0, charger.maxCurrentAmps)
|
result + max(0, charger.maxCurrentAmps)
|
||||||
@@ -611,8 +625,8 @@ struct SystemOverviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var estimatedRuntimeHours: Double? {
|
private var estimatedRuntimeHours: Double? {
|
||||||
guard totalPower > 0, totalEnergy > 0 else { return nil }
|
guard totalPower > 0, totalUsableEnergy > 0 else { return nil }
|
||||||
let hours = totalEnergy / totalPower
|
let hours = totalUsableEnergy / totalPower
|
||||||
return hours.isFinite && hours > 0 ? hours : nil
|
return hours.isFinite && hours > 0 ? hours : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,12 +752,21 @@ struct SystemOverviewView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var batteryEnergyLabel: String {
|
private var batteryUsableCapacityLabel: String {
|
||||||
NSLocalizedString(
|
NSLocalizedString(
|
||||||
"battery.bank.metric.energy",
|
"battery.bank.metric.usable_capacity",
|
||||||
bundle: .main,
|
bundle: .main,
|
||||||
value: "Energy",
|
value: "Usable Capacity",
|
||||||
comment: "Label for total energy metric"
|
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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class SavedBattery {
|
|||||||
var name: String
|
var name: String
|
||||||
var nominalVoltage: Double
|
var nominalVoltage: Double
|
||||||
var capacityAmpHours: Double
|
var capacityAmpHours: Double
|
||||||
|
var usableCapacityOverrideFraction: Double?
|
||||||
private var chemistryRawValue: String
|
private var chemistryRawValue: String
|
||||||
var iconName: String = "battery.100"
|
var iconName: String = "battery.100"
|
||||||
var colorName: String = "blue"
|
var colorName: String = "blue"
|
||||||
@@ -19,6 +20,7 @@ class SavedBattery {
|
|||||||
nominalVoltage: Double = 12.8,
|
nominalVoltage: Double = 12.8,
|
||||||
capacityAmpHours: Double = 100,
|
capacityAmpHours: Double = 100,
|
||||||
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
|
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
|
||||||
|
usableCapacityOverrideFraction: Double? = nil,
|
||||||
iconName: String = "battery.100",
|
iconName: String = "battery.100",
|
||||||
colorName: String = "blue",
|
colorName: String = "blue",
|
||||||
system: ElectricalSystem? = nil,
|
system: ElectricalSystem? = nil,
|
||||||
@@ -28,6 +30,7 @@ class SavedBattery {
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.nominalVoltage = nominalVoltage
|
self.nominalVoltage = nominalVoltage
|
||||||
self.capacityAmpHours = capacityAmpHours
|
self.capacityAmpHours = capacityAmpHours
|
||||||
|
self.usableCapacityOverrideFraction = usableCapacityOverrideFraction
|
||||||
self.chemistryRawValue = chemistry.rawValue
|
self.chemistryRawValue = chemistry.rawValue
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.colorName = colorName
|
self.colorName = colorName
|
||||||
@@ -47,4 +50,18 @@ class SavedBattery {
|
|||||||
var energyWattHours: Double {
|
var energyWattHours: Double {
|
||||||
nominalVoltage * capacityAmpHours
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,26 +25,18 @@ struct SettingsView: View {
|
|||||||
.pickerStyle(.segmented)
|
.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 {
|
Section {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
@@ -88,4 +80,10 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#Preview("Settings (Default)") {
|
||||||
|
let settings = UnitSystemSettings()
|
||||||
|
return SettingsView()
|
||||||
|
.environmentObject(settings)
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,9 +46,15 @@ class UnitSystemSettings: ObservableObject {
|
|||||||
UserDefaults.standard.set(unitSystem.rawValue, forKey: "unitSystem")
|
UserDefaults.standard.set(unitSystem.rawValue, forKey: "unitSystem")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Published var isProUnlocked: Bool {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(isProUnlocked, forKey: "isProUnlocked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let savedSystem = UserDefaults.standard.string(forKey: "unitSystem") ?? UnitSystem.metric.rawValue
|
let savedSystem = UserDefaults.standard.string(forKey: "unitSystem") ?? UnitSystem.metric.rawValue
|
||||||
self.unitSystem = UnitSystem(rawValue: savedSystem) ?? .metric
|
self.unitSystem = UnitSystem(rawValue: savedSystem) ?? .metric
|
||||||
|
self.isProUnlocked = UserDefaults.standard.bool(forKey: "isProUnlocked")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,18 @@
|
|||||||
"slider.length.title" = "Kabellänge (%@)";
|
"slider.length.title" = "Kabellänge (%@)";
|
||||||
"slider.power.title" = "Leistung";
|
"slider.power.title" = "Leistung";
|
||||||
"slider.voltage.title" = "Spannung";
|
"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";
|
"system.list.no.components" = "Noch keine Verbraucher";
|
||||||
"units.imperial.display" = "Imperial (AWG, ft)";
|
"units.imperial.display" = "Imperial (AWG, ft)";
|
||||||
"units.metric.display" = "Metrisch (mm², m)";
|
"units.metric.display" = "Metrisch (mm², m)";
|
||||||
@@ -172,6 +184,8 @@
|
|||||||
"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.bank.metric.usable_capacity" = "Nutzbare Kapazität";
|
||||||
|
"battery.bank.metric.usable_energy" = "Nutzbare 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.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.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.section.summary" = "Übersicht";
|
||||||
"battery.editor.slider.voltage" = "Nennspannung";
|
"battery.editor.slider.voltage" = "Nennspannung";
|
||||||
"battery.editor.slider.capacity" = "Kapazität";
|
"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.title" = "Nennspannung bearbeiten";
|
||||||
"battery.editor.alert.voltage.placeholder" = "Spannung";
|
"battery.editor.alert.voltage.placeholder" = "Spannung";
|
||||||
"battery.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
|
"battery.editor.alert.voltage.message" = "Spannung in Volt (V) eingeben";
|
||||||
"battery.editor.alert.capacity.title" = "Kapazität bearbeiten";
|
"battery.editor.alert.capacity.title" = "Kapazität bearbeiten";
|
||||||
"battery.editor.alert.capacity.placeholder" = "Kapazität";
|
"battery.editor.alert.capacity.placeholder" = "Kapazität";
|
||||||
"battery.editor.alert.capacity.message" = "Kapazität in Amperestunden (Ah) eingeben";
|
"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.cancel" = "Abbrechen";
|
||||||
"battery.editor.alert.save" = "Speichern";
|
"battery.editor.alert.save" = "Speichern";
|
||||||
"battery.editor.default_name" = "Neue Batterie";
|
"battery.editor.default_name" = "Neue Batterie";
|
||||||
|
|||||||
@@ -33,6 +33,18 @@
|
|||||||
"slider.length.title" = "Longitud del cable (%@)";
|
"slider.length.title" = "Longitud del cable (%@)";
|
||||||
"slider.power.title" = "Potencia";
|
"slider.power.title" = "Potencia";
|
||||||
"slider.voltage.title" = "Voltaje";
|
"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";
|
"system.list.no.components" = "Aún no hay componentes";
|
||||||
"units.imperial.display" = "Imperial (AWG, ft)";
|
"units.imperial.display" = "Imperial (AWG, ft)";
|
||||||
"units.metric.display" = "Métrico (mm², m)";
|
"units.metric.display" = "Métrico (mm², m)";
|
||||||
@@ -171,6 +183,8 @@
|
|||||||
"battery.bank.metric.count" = "Baterías";
|
"battery.bank.metric.count" = "Baterías";
|
||||||
"battery.bank.metric.capacity" = "Capacidad";
|
"battery.bank.metric.capacity" = "Capacidad";
|
||||||
"battery.bank.metric.energy" = "Energía";
|
"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.overview.empty.create" = "Añadir batería";
|
||||||
"battery.onboarding.title" = "Añade tu primera 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.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.section.summary" = "Resumen";
|
||||||
"battery.editor.slider.voltage" = "Voltaje nominal";
|
"battery.editor.slider.voltage" = "Voltaje nominal";
|
||||||
"battery.editor.slider.capacity" = "Capacidad";
|
"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.title" = "Editar voltaje nominal";
|
||||||
"battery.editor.alert.voltage.placeholder" = "Voltaje";
|
"battery.editor.alert.voltage.placeholder" = "Voltaje";
|
||||||
"battery.editor.alert.voltage.message" = "Introduce el voltaje en voltios (V)";
|
"battery.editor.alert.voltage.message" = "Introduce el voltaje en voltios (V)";
|
||||||
"battery.editor.alert.capacity.title" = "Editar capacidad";
|
"battery.editor.alert.capacity.title" = "Editar capacidad";
|
||||||
"battery.editor.alert.capacity.placeholder" = "Capacidad";
|
"battery.editor.alert.capacity.placeholder" = "Capacidad";
|
||||||
"battery.editor.alert.capacity.message" = "Introduce la capacidad en amperios-hora (Ah)";
|
"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.cancel" = "Cancelar";
|
||||||
"battery.editor.alert.save" = "Guardar";
|
"battery.editor.alert.save" = "Guardar";
|
||||||
"battery.editor.default_name" = "Nueva batería";
|
"battery.editor.default_name" = "Nueva batería";
|
||||||
|
|||||||
@@ -33,6 +33,18 @@
|
|||||||
"slider.length.title" = "Longueur du câble (%@)";
|
"slider.length.title" = "Longueur du câble (%@)";
|
||||||
"slider.power.title" = "Puissance";
|
"slider.power.title" = "Puissance";
|
||||||
"slider.voltage.title" = "Tension";
|
"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";
|
"system.list.no.components" = "Aucun composant pour l'instant";
|
||||||
"units.imperial.display" = "Impérial (AWG, ft)";
|
"units.imperial.display" = "Impérial (AWG, ft)";
|
||||||
"units.metric.display" = "Métrique (mm², m)";
|
"units.metric.display" = "Métrique (mm², m)";
|
||||||
@@ -171,6 +183,8 @@
|
|||||||
"battery.bank.metric.count" = "Batteries";
|
"battery.bank.metric.count" = "Batteries";
|
||||||
"battery.bank.metric.capacity" = "Capacité";
|
"battery.bank.metric.capacity" = "Capacité";
|
||||||
"battery.bank.metric.energy" = "Énergie";
|
"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.overview.empty.create" = "Ajouter une batterie";
|
||||||
"battery.onboarding.title" = "Ajoutez votre première 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.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.section.summary" = "Résumé";
|
||||||
"battery.editor.slider.voltage" = "Tension nominale";
|
"battery.editor.slider.voltage" = "Tension nominale";
|
||||||
"battery.editor.slider.capacity" = "Capacité";
|
"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.title" = "Modifier la tension nominale";
|
||||||
"battery.editor.alert.voltage.placeholder" = "Tension";
|
"battery.editor.alert.voltage.placeholder" = "Tension";
|
||||||
"battery.editor.alert.voltage.message" = "Saisissez la tension en volts (V)";
|
"battery.editor.alert.voltage.message" = "Saisissez la tension en volts (V)";
|
||||||
"battery.editor.alert.capacity.title" = "Modifier la capacité";
|
"battery.editor.alert.capacity.title" = "Modifier la capacité";
|
||||||
"battery.editor.alert.capacity.placeholder" = "Capacité";
|
"battery.editor.alert.capacity.placeholder" = "Capacité";
|
||||||
"battery.editor.alert.capacity.message" = "Saisissez la capacité en ampères-heures (Ah)";
|
"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.cancel" = "Annuler";
|
||||||
"battery.editor.alert.save" = "Enregistrer";
|
"battery.editor.alert.save" = "Enregistrer";
|
||||||
"battery.editor.default_name" = "Nouvelle batterie";
|
"battery.editor.default_name" = "Nouvelle batterie";
|
||||||
|
|||||||
@@ -33,6 +33,18 @@
|
|||||||
"slider.length.title" = "Kabellengte (%@)";
|
"slider.length.title" = "Kabellengte (%@)";
|
||||||
"slider.power.title" = "Vermogen";
|
"slider.power.title" = "Vermogen";
|
||||||
"slider.voltage.title" = "Spanning";
|
"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";
|
"system.list.no.components" = "Nog geen componenten";
|
||||||
"units.imperial.display" = "Imperiaal (AWG, ft)";
|
"units.imperial.display" = "Imperiaal (AWG, ft)";
|
||||||
"units.metric.display" = "Metrisch (mm², m)";
|
"units.metric.display" = "Metrisch (mm², m)";
|
||||||
@@ -171,6 +183,8 @@
|
|||||||
"battery.bank.metric.count" = "Batterijen";
|
"battery.bank.metric.count" = "Batterijen";
|
||||||
"battery.bank.metric.capacity" = "Capaciteit";
|
"battery.bank.metric.capacity" = "Capaciteit";
|
||||||
"battery.bank.metric.energy" = "Energie";
|
"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.overview.empty.create" = "Accu toevoegen";
|
||||||
"battery.onboarding.title" = "Voeg je eerste accu toe";
|
"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.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.section.summary" = "Overzicht";
|
||||||
"battery.editor.slider.voltage" = "Nominale spanning";
|
"battery.editor.slider.voltage" = "Nominale spanning";
|
||||||
"battery.editor.slider.capacity" = "Capaciteit";
|
"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.title" = "Nominale spanning bewerken";
|
||||||
"battery.editor.alert.voltage.placeholder" = "Spanning";
|
"battery.editor.alert.voltage.placeholder" = "Spanning";
|
||||||
"battery.editor.alert.voltage.message" = "Voer de spanning in volt (V) in";
|
"battery.editor.alert.voltage.message" = "Voer de spanning in volt (V) in";
|
||||||
"battery.editor.alert.capacity.title" = "Capaciteit bewerken";
|
"battery.editor.alert.capacity.title" = "Capaciteit bewerken";
|
||||||
"battery.editor.alert.capacity.placeholder" = "Capaciteit";
|
"battery.editor.alert.capacity.placeholder" = "Capaciteit";
|
||||||
"battery.editor.alert.capacity.message" = "Voer de capaciteit in ampère-uur (Ah) in";
|
"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.cancel" = "Annuleren";
|
||||||
"battery.editor.alert.save" = "Opslaan";
|
"battery.editor.alert.save" = "Opslaan";
|
||||||
"battery.editor.default_name" = "Nieuwe batterij";
|
"battery.editor.default_name" = "Nieuwe batterij";
|
||||||
|
|||||||
Reference in New Issue
Block a user