From 46664625b457c575fdb22341ac6f754fde2ef5de Mon Sep 17 00:00:00 2001 From: Stefan Lange-Hegermann Date: Tue, 28 Oct 2025 13:31:51 +0100 Subject: [PATCH] some advanced settings --- Cable/Base.lproj/Localizable.strings | 22 + Cable/Batteries/BatteriesView.swift | 158 +++--- Cable/Batteries/BatteryConfiguration.swift | 45 ++ Cable/Loads/CableCalculator.swift | 26 +- Cable/Loads/CalculatorView.swift | 527 +++++++++++++++++---- Cable/Overview/SystemOverviewView.swift | 107 +++-- Cable/SavedBattery.swift | 17 + Cable/SettingsView.swift | 38 +- Cable/UnitSystem.swift | 6 + Cable/de.lproj/Localizable.strings | 22 + Cable/es.lproj/Localizable.strings | 22 + Cable/fr.lproj/Localizable.strings | 22 + Cable/nl.lproj/Localizable.strings | 22 + 13 files changed, 815 insertions(+), 219 deletions(-) diff --git a/Cable/Base.lproj/Localizable.strings b/Cable/Base.lproj/Localizable.strings index 1fd375d..7070820 100644 --- a/Cable/Base.lproj/Localizable.strings +++ b/Cable/Base.lproj/Localizable.strings @@ -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"; diff --git a/Cable/Batteries/BatteriesView.swift b/Cable/Batteries/BatteriesView.swift index ca0425f..4c55f5c 100644 --- a/Cable/Batteries/BatteriesView.swift +++ b/Cable/Batteries/BatteriesView.swift @@ -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( diff --git a/Cable/Batteries/BatteryConfiguration.swift b/Cable/Batteries/BatteryConfiguration.swift index 180f408..edaa3b7 100644 --- a/Cable/Batteries/BatteryConfiguration.swift +++ b/Cable/Batteries/BatteryConfiguration.swift @@ -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) diff --git a/Cable/Loads/CableCalculator.swift b/Cable/Loads/CableCalculator.swift index 6c8e270..7e37745 100644 --- a/Cable/Loads/CableCalculator.swift +++ b/Cable/Loads/CableCalculator.swift @@ -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 diff --git a/Cable/Loads/CalculatorView.swift b/Cable/Loads/CalculatorView.swift index f4ab0ec..c35b6e1 100644 --- a/Cable/Loads/CalculatorView.swift +++ b/Cable/Loads/CalculatorView.swift @@ -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 @@ -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(_ 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(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(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(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(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(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(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(_ 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(_ 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 { @@ -788,6 +1038,14 @@ struct CalculatorView: View { return 0...upperBound } + private var dutyCycleRange: ClosedRange { + 0...100 + } + + private var usageHoursRange: ClosedRange { + 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) { diff --git a/Cable/Overview/SystemOverviewView.swift b/Cable/Overview/SystemOverviewView.swift index cf5ce5a..2ec6426 100644 --- a/Cable/Overview/SystemOverviewView.swift +++ b/Cable/Overview/SystemOverviewView.swift @@ -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" ) } diff --git a/Cable/SavedBattery.swift b/Cable/SavedBattery.swift index 721b583..2c3e3b7 100644 --- a/Cable/SavedBattery.swift +++ b/Cable/SavedBattery.swift @@ -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 + } } diff --git a/Cable/SettingsView.swift b/Cable/SettingsView.swift index fc9fddb..92434f5 100644 --- a/Cable/SettingsView.swift +++ b/Cable/SettingsView.swift @@ -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 { } } } -} \ No newline at end of file +} + +#Preview("Settings (Default)") { + let settings = UnitSystemSettings() + return SettingsView() + .environmentObject(settings) +} diff --git a/Cable/UnitSystem.swift b/Cable/UnitSystem.swift index beec220..ba48c10 100644 --- a/Cable/UnitSystem.swift +++ b/Cable/UnitSystem.swift @@ -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") } } diff --git a/Cable/de.lproj/Localizable.strings b/Cable/de.lproj/Localizable.strings index 7dd45a2..a2831e5 100644 --- a/Cable/de.lproj/Localizable.strings +++ b/Cable/de.lproj/Localizable.strings @@ -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"; diff --git a/Cable/es.lproj/Localizable.strings b/Cable/es.lproj/Localizable.strings index 15b1c1d..caca2e4 100644 --- a/Cable/es.lproj/Localizable.strings +++ b/Cable/es.lproj/Localizable.strings @@ -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"; diff --git a/Cable/fr.lproj/Localizable.strings b/Cable/fr.lproj/Localizable.strings index 9a9c3a6..afadfc1 100644 --- a/Cable/fr.lproj/Localizable.strings +++ b/Cable/fr.lproj/Localizable.strings @@ -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"; diff --git a/Cable/nl.lproj/Localizable.strings b/Cable/nl.lproj/Localizable.strings index 1b5ece9..e5b8727 100644 --- a/Cable/nl.lproj/Localizable.strings +++ b/Cable/nl.lproj/Localizable.strings @@ -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";