some advanced settings
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user