much better system overview

This commit is contained in:
Stefan Lange-Hegermann
2025-10-29 12:55:47 +01:00
parent 45a462295d
commit 97a9d3903c
8 changed files with 650 additions and 93 deletions

View File

@@ -180,6 +180,22 @@
"overview.runtime.title" = "Estimated runtime";
"overview.runtime.unavailable" = "Add battery capacity and load power to estimate runtime.";
"overview.system.header.title" = "System Overview";
"overview.bom.title" = "Bill of Materials";
"overview.bom.subtitle" = "Tap to review components";
"overview.bom.unavailable" = "Add loads to generate components.";
"overview.bom.placeholder.short" = "Add loads";
"overview.chargetime.title" = "Estimated charge time";
"overview.chargetime.subtitle" = "At combined charge rate";
"overview.chargetime.unavailable" = "Add chargers and battery capacity to estimate.";
"overview.chargetime.placeholder.short" = "Add chargers";
"overview.goal.prefix" = "Goal";
"overview.goal.label" = "Goal %@";
"overview.goal.clear" = "Remove Goal";
"overview.goal.cancel" = "Cancel";
"overview.goal.save" = "Save";
"overview.runtime.goal.title" = "Runtime Goal";
"overview.runtime.placeholder.short" = "Add capacity";
"overview.chargetime.goal.title" = "Charge Goal";
"sample.battery.rv.name" = "LiFePO4 house bank";
"sample.battery.workshop.name" = "Workbench backup battery";
"sample.charger.dcdc.name" = "DC-DC charger";

View File

@@ -112,13 +112,24 @@ class ElectricalSystem {
var timestamp: Date = Date()
var iconName: String = "building.2"
var colorName: String = "blue"
var targetRuntimeHours: Double?
var targetChargeTimeHours: Double?
init(name: String, location: String = "", iconName: String = "building.2", colorName: String = "blue") {
init(
name: String,
location: String = "",
iconName: String = "building.2",
colorName: String = "blue",
targetRuntimeHours: Double? = nil,
targetChargeTimeHours: Double? = nil
) {
self.name = name
self.location = location
self.timestamp = Date()
self.iconName = iconName
self.colorName = colorName
self.targetRuntimeHours = targetRuntimeHours
self.targetChargeTimeHours = targetChargeTimeHours
}
}

View File

@@ -288,6 +288,7 @@ struct LoadsView: View {
onSelectChargers: { selectedComponentTab = .chargers },
onCreateLoad: { createNewLoad() },
onBrowseLibrary: { showingComponentLibrary = true },
onShowBillOfMaterials: { showingSystemBOM = true },
onCreateBattery: { startBatteryConfiguration() },
onCreateCharger: { startChargerConfiguration() }
)

View File

@@ -1,9 +1,15 @@
import SwiftUI
import SwiftData
struct SystemOverviewView: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.modelContext) private var modelContext
@State private var activeStatus: LoadConfigurationStatus?
@State private var suppressLoadNavigation = false
@State private var showingRuntimeGoalEditor = false
@State private var showingChargeGoalEditor = false
@State private var runtimeGoalDraftHours: Double = 4
@State private var chargeGoalDraftHours: Double = 4
let system: ElectricalSystem
let loads: [SavedLoad]
let batteries: [SavedBattery]
@@ -13,85 +19,205 @@ struct SystemOverviewView: View {
let onSelectChargers: () -> Void
let onCreateLoad: () -> Void
let onBrowseLibrary: () -> Void
let onShowBillOfMaterials: () -> Void
let onCreateBattery: () -> Void
let onCreateCharger: () -> Void
var body: some View {
ScrollView {
VStack(spacing: 16) {
systemCard
loadsCard
batteriesCard
chargersCard
VStack(spacing: 0) {
systemCard
.padding(.horizontal, 16)
.padding(.top, 20)
.padding(.bottom, 16)
ScrollView {
VStack(spacing: 16) {
loadsCard
batteriesCard
chargersCard
}
.padding(.horizontal, 16)
.padding(.bottom, 20)
}
.padding(.horizontal, 16)
.padding(.vertical, 20)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.sheet(isPresented: $showingRuntimeGoalEditor) {
GoalEditorSheet(
title: runtimeGoalSheetTitle,
tint: .orange,
value: $runtimeGoalDraftHours,
minimum: Self.minimumGoalHours,
maximum: Self.maximumGoalHours,
step: Self.goalStepHours,
cancelTitle: goalCancelTitle,
saveTitle: goalSaveTitle,
clearTitle: goalClearTitle,
showsClear: system.targetRuntimeHours != nil,
formattedDurationProvider: { formattedDuration(hours: $0) },
onSave: { saveRuntimeGoal(hours: $0) },
onClear: system.targetRuntimeHours != nil ? { clearRuntimeGoal() } : nil
)
}
.sheet(isPresented: $showingChargeGoalEditor) {
GoalEditorSheet(
title: chargeGoalSheetTitle,
tint: .blue,
value: $chargeGoalDraftHours,
minimum: Self.minimumGoalHours,
maximum: Self.maximumGoalHours,
step: Self.goalStepHours,
cancelTitle: goalCancelTitle,
saveTitle: goalSaveTitle,
clearTitle: goalClearTitle,
showsClear: system.targetChargeTimeHours != nil,
formattedDurationProvider: { formattedDuration(hours: $0) },
onSave: { saveChargeGoal(hours: $0) },
onClear: system.targetChargeTimeHours != nil ? { clearChargeGoal() } : nil
)
}
}
private var systemCard: some View {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 18) {
Text(systemOverviewTitle)
.font(.headline.weight(.semibold))
ViewThatFits(in: .horizontal) {
HStack(spacing: 16) {
summaryMetric(
icon: "square.stack.3d.up",
label: loadsCountLabel,
value: "\(loads.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.fill",
label: chargerCountLabel,
value: "\(chargers.count)",
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
}
VStack(spacing: 14) {
overviewMetricRow(
title: runtimeTitle,
subtitle: runtimeSubtitle,
icon: "clock.arrow.circlepath",
tint: .orange,
value: formattedRuntime,
placeholder: runtimePlaceholderSummary,
goal: runtimeGoalValueText,
actualHours: estimatedRuntimeHours,
goalHours: system.targetRuntimeHours,
progressFraction: nil,
hasValue: formattedRuntime != nil,
action: openRuntimeGoalEditor
)
VStack(alignment: .leading, spacing: 12) {
summaryMetric(
icon: "square.stack.3d.up",
label: loadsCountLabel,
value: "\(loads.count)",
tint: .blue
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "battery.100",
label: batteryCountLabel,
value: "\(batteries.count)",
tint: .green
).frame(maxWidth: .infinity, alignment: .leading)
summaryMetric(
icon: "bolt.fill",
label: chargerCountLabel,
value: "\(chargers.count)",
tint: .orange
).frame(maxWidth: .infinity, alignment: .leading)
}
overviewMetricRow(
title: bomTitle,
subtitle: bomSubtitle,
icon: "list.bullet.rectangle",
tint: .purple,
value: formattedBOMCompletedCount,
placeholder: bomPlaceholderSummary,
goal: formattedBOMTotalCount,
actualHours: nil,
goalHours: nil,
progressFraction: bomCompletionFraction,
hasValue: bomItemsCount > 0,
action: onShowBillOfMaterials
)
overviewMetricRow(
title: chargeTimeTitle,
subtitle: chargeTimeSubtitle,
icon: "bolt.fill",
tint: .blue,
value: formattedChargeTime,
placeholder: chargeTimePlaceholderSummary,
goal: chargeGoalValueText,
actualHours: estimatedChargeHours,
goalHours: system.targetChargeTimeHours,
progressFraction: nil,
hasValue: formattedChargeTime != nil,
action: openChargeGoalEditor
)
}
runtimeSection
.padding(.top, 4)
}
.padding(.horizontal, 16)
.padding(.vertical, 18)
.padding(.horizontal, 20)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.systemBackground))
.stroke(Color(.separator).opacity(0.18), lineWidth: 1)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.systemBackground))
)
)
}
@ViewBuilder
private func overviewMetricRow(
title: String,
subtitle: String?,
icon: String,
tint: Color,
value: String?,
placeholder: String?,
goal: String?,
actualHours: Double?,
goalHours: Double?,
progressFraction: Double?,
hasValue: Bool,
action: (() -> Void)? = nil
) -> some View {
let content = VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .center, spacing: 12) {
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(tint.opacity(0.18))
.frame(width: 24, height: 24)
.overlay {
Image(systemName: icon)
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(tint)
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.subheadline.weight(.semibold))
.lineLimit(1)
if let subtitle {
Text(subtitle)
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text(value ?? "")
.font(.headline.weight(.semibold))
.foregroundStyle(hasValue ? Color.primary : Color.secondary)
.lineLimit(1)
if let goal {
Text("\(goalPrefix) \(goal)")
.font(.caption2)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
progressBar(
progress: adjustedProgress(for: progressFraction, actualHours: actualHours, goalHours: goalHours),
tint: tint
)
}
.padding(.vertical, 2)
let paddedContent = content
.padding(.horizontal, 4)
if let action {
Button(action: action) {
paddedContent
}
.buttonStyle(.plain)
} else {
paddedContent
}
}
@ViewBuilder
private var loadsCard: some View {
if loads.isEmpty {
@@ -531,37 +657,6 @@ struct SystemOverviewView: View {
}
}
@ViewBuilder
private var runtimeSection: some View {
if let runtimeText = formattedRuntime {
HStack(alignment: .center, spacing: 12) {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: 20, weight: .semibold))
.foregroundStyle(Color.orange)
VStack(alignment: .leading, spacing: 4) {
Text(runtimeTitle)
.font(.subheadline.weight(.semibold))
Text(runtimeSubtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text(runtimeText)
.font(.title3.weight(.semibold))
}
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.orange.opacity(0.12))
)
} else if shouldShowRuntimeHint {
Text(runtimeUnavailableText)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
private func statusBanner(for status: LoadConfigurationStatus) -> some View {
HStack(spacing: 10) {
Image(systemName: status.symbol)
@@ -580,6 +675,41 @@ struct SystemOverviewView: View {
)
}
private var bomCompletionFraction: Double? {
guard bomItemsCount > 0 else { return nil }
return Double(completedBOMItemCount) / Double(bomItemsCount)
}
private var completedBOMItemCount: Int {
settledLoads.reduce(into: Set<String>()) { partialResult, load in
load.bomCompletedItemIDs.forEach { partialResult.insert($0) }
}.count
}
private var bomItemsCount: Int {
settledLoads.isEmpty ? 0 : settledLoads.count * Self.bomItemsPerLoad
}
private var settledLoads: [SavedLoad] {
loads.filter { $0.crossSection > 0 && $0.length > 0 }
}
private func progressBar(progress: CGFloat?, tint: Color) -> some View {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.fill(Color(.tertiarySystemFill))
.frame(height: 4)
.frame(maxWidth: .infinity)
.overlay(alignment: .leading) {
if let progress {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.fill(tint)
.frame(maxWidth: .infinity)
.scaleEffect(x: max(min(progress, 1), 0), y: 1, anchor: .leading)
.animation(.easeInOut(duration: 0.25), value: progress)
}
}
}
private func formattedCurrent(_ value: Double) -> String {
let numberString = Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
return "\(numberString) A"
@@ -590,6 +720,10 @@ struct SystemOverviewView: View {
return "\(numberString) W"
}
private func numberString(for value: Double) -> String {
Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.0f", value)
}
private func formattedChargerOutput(_ value: Double?) -> String {
guard let value, value > 0 else { return "" }
return formattedValue(value, unit: "V")
@@ -600,6 +734,32 @@ struct SystemOverviewView: View {
return "\(numberString) \(unit)"
}
private func progressFraction(actualHours: Double?, goalHours: Double?) -> CGFloat {
guard let actualHours, let goalHours, goalHours > 0 else { return 0 }
let ratio = actualHours / goalHours
return CGFloat(max(min(ratio, 1), 0))
}
private func adjustedProgress(for explicit: Double?, actualHours: Double?, goalHours: Double?) -> CGFloat? {
guard let ratio = progressRatio(explicitProgress: explicit, actualHours: actualHours, goalHours: goalHours) else { return nil }
if explicit != nil { return ratio }
guard let actual = actualHours, let goal = goalHours, goal > 0 else { return ratio }
if goal == system.targetChargeTimeHours {
let value = goal / max(actual, 0.0001)
return CGFloat(max(min(value, 1), 0))
}
return ratio
}
private func progressRatio(explicitProgress: Double?, actualHours: Double?, goalHours: Double?) -> CGFloat? {
if let explicitProgress {
return CGFloat(max(min(explicitProgress, 1), 0))
}
guard goalHours != nil else { return nil }
return progressFraction(actualHours: actualHours, goalHours: goalHours)
}
private var estimatedRuntimeHours: Double? {
guard totalPower > 0, totalUsableEnergy > 0 else { return nil }
let hours = totalUsableEnergy / totalPower
@@ -615,9 +775,99 @@ struct SystemOverviewView: View {
let numberString = Self.numberFormatter.string(from: NSNumber(value: hours)) ?? String(format: "%.1f", hours)
return "\(numberString) h"
}
private var formattedChargeTime: String? {
guard let hours = estimatedChargeHours else { return nil }
let seconds = hours * 3600
if let formatted = Self.runtimeFormatter.string(from: seconds) {
return formatted
}
let numberString = Self.numberFormatter.string(from: NSNumber(value: hours)) ?? String(format: "%.1f", hours)
return "\(numberString) h"
}
private var shouldShowRuntimeHint: Bool {
!batteries.isEmpty && !loads.isEmpty && formattedRuntime == nil
private var estimatedChargeHours: Double? {
guard totalUsableCapacity > 0, totalChargerCurrent > 0 else { return nil }
let hours = totalUsableCapacity / totalChargerCurrent
return hours.isFinite && hours > 0 ? hours : nil
}
private func openRuntimeGoalEditor() {
runtimeGoalDraftHours = defaultGoalHours(current: system.targetRuntimeHours, fallback: estimatedRuntimeHours)
showingRuntimeGoalEditor = true
}
private func openChargeGoalEditor() {
chargeGoalDraftHours = defaultGoalHours(current: system.targetChargeTimeHours, fallback: estimatedChargeHours)
showingChargeGoalEditor = true
}
private func saveRuntimeGoal(hours: Double) {
let adjusted = clampedGoal(hours)
system.targetRuntimeHours = adjusted
runtimeGoalDraftHours = adjusted
persistSystemChanges()
showingRuntimeGoalEditor = false
}
private func saveChargeGoal(hours: Double) {
let adjusted = clampedGoal(hours)
system.targetChargeTimeHours = adjusted
chargeGoalDraftHours = adjusted
persistSystemChanges()
showingChargeGoalEditor = false
}
private func clearRuntimeGoal() {
system.targetRuntimeHours = nil
persistSystemChanges()
runtimeGoalDraftHours = defaultGoalHours(current: nil, fallback: estimatedRuntimeHours)
showingRuntimeGoalEditor = false
}
private func clearChargeGoal() {
system.targetChargeTimeHours = nil
persistSystemChanges()
chargeGoalDraftHours = defaultGoalHours(current: nil, fallback: estimatedChargeHours)
showingChargeGoalEditor = false
}
private func persistSystemChanges() {
guard modelContext.hasChanges else { return }
do {
try modelContext.save()
} catch {
#if DEBUG
print("Failed to save system goal: \(error.localizedDescription)")
#endif
}
}
private func defaultGoalHours(current: Double?, fallback: Double?) -> Double {
if let current, current >= Self.minimumGoalHours {
return clampedGoal(current)
}
if let fallback, fallback >= Self.minimumGoalHours {
return clampedGoal(fallback)
}
return Self.fallbackGoalHours
}
private func clampedGoal(_ hours: Double) -> Double {
guard hours.isFinite else { return Self.fallbackGoalHours }
let rounded = (hours / Self.goalStepHours).rounded() * Self.goalStepHours
return min(max(rounded, Self.minimumGoalHours), Self.maximumGoalHours)
}
private func formattedDuration(hours: Double) -> String {
let seconds = hours * 3600
if let formatted = Self.runtimeFormatter.string(from: seconds) {
return formatted
}
let numberString = Self.numberFormatter.string(from: NSNumber(value: hours)) ?? String(format: "%.1f", hours)
return "\(numberString) h"
}
private var loadsSummaryTitle: String {
@@ -856,6 +1106,83 @@ struct SystemOverviewView: View {
)
}
private var bomTitle: String {
NSLocalizedString(
"overview.bom.title",
bundle: .main,
value: "Bill of Materials",
comment: "Title for BOM metric card in the system overview"
)
}
private var bomSubtitle: String {
NSLocalizedString(
"overview.bom.subtitle",
bundle: .main,
value: "Tap to review components",
comment: "Subtitle describing the BOM metric card interaction"
)
}
private var bomPlaceholderSummary: String {
NSLocalizedString(
"overview.bom.placeholder.short",
bundle: .main,
value: "Add loads",
comment: "Short placeholder shown when no BOM data is available"
)
}
private var formattedBOMCompletedCount: String? {
guard bomItemsCount > 0 else { return nil }
return numberString(for: Double(completedBOMItemCount))
}
private var formattedBOMTotalCount: String? {
guard bomItemsCount > 0 else { return nil }
return numberString(for: Double(bomItemsCount))
}
private var chargeTimeTitle: String {
NSLocalizedString(
"overview.chargetime.title",
bundle: .main,
value: "Estimated charge time",
comment: "Title for the charge time metric card"
)
}
private var chargeTimeSubtitle: String {
NSLocalizedString(
"overview.chargetime.subtitle",
bundle: .main,
value: "At combined charge rate",
comment: "Subtitle describing charge time assumptions"
)
}
private var chargeTimePlaceholderSummary: String {
NSLocalizedString(
"overview.chargetime.placeholder.short",
bundle: .main,
value: "Add chargers",
comment: "Short placeholder shown when charge time cannot be calculated"
)
}
private var chargeGoalValueText: String? {
formattedGoalValue(for: system.targetChargeTimeHours)
}
private var goalPrefix: String {
NSLocalizedString(
"overview.goal.prefix",
bundle: .main,
value: "Goal",
comment: "Prefix displayed before goal value"
)
}
private var runtimeTitle: String {
NSLocalizedString(
"overview.runtime.title",
@@ -874,12 +1201,66 @@ struct SystemOverviewView: View {
)
}
private var runtimeUnavailableText: String {
private var runtimePlaceholderSummary: String {
NSLocalizedString(
"overview.runtime.unavailable",
"overview.runtime.placeholder.short",
bundle: .main,
value: "Add battery capacity and load power to estimate runtime.",
comment: "Message shown when runtime cannot be calculated"
value: "Add capacity",
comment: "Short placeholder shown when runtime cannot be calculated"
)
}
private var runtimeGoalValueText: String? {
formattedGoalValue(for: system.targetRuntimeHours)
}
private func formattedGoalValue(for hours: Double?) -> String? {
guard let hours else { return nil }
return formattedDuration(hours: hours)
}
private var runtimeGoalSheetTitle: String {
NSLocalizedString(
"overview.runtime.goal.title",
bundle: .main,
value: "Runtime Goal",
comment: "Navigation title for editing the runtime goal"
)
}
private var chargeGoalSheetTitle: String {
NSLocalizedString(
"overview.chargetime.goal.title",
bundle: .main,
value: "Charge Goal",
comment: "Navigation title for editing the charge time goal"
)
}
private var goalClearTitle: String {
NSLocalizedString(
"overview.goal.clear",
bundle: .main,
value: "Remove Goal",
comment: "Button title to clear an active goal"
)
}
private var goalCancelTitle: String {
NSLocalizedString(
"overview.goal.cancel",
bundle: .main,
value: "Cancel",
comment: "Button title to cancel goal editing"
)
}
private var goalSaveTitle: String {
NSLocalizedString(
"overview.goal.save",
bundle: .main,
value: "Save",
comment: "Button title to save goal editing"
)
}
@@ -899,6 +1280,13 @@ struct SystemOverviewView: View {
return formatter
}()
private static let minimumGoalHours: Double = 0.5
private static let maximumGoalHours: Double = 240
private static let goalStepHours: Double = 0.25
private static let fallbackGoalHours: Double = 4
private static let bomItemsPerLoad = 5
private enum BatteryWarning {
case voltage(count: Int)
case capacity(count: Int)
@@ -942,6 +1330,77 @@ struct SystemOverviewView: View {
}
}
private struct GoalEditorSheet: View {
let title: String
let tint: Color
@Binding var value: Double
let minimum: Double
let maximum: Double
let step: Double
let cancelTitle: String
let saveTitle: String
let clearTitle: String
let showsClear: Bool
let formattedDurationProvider: (Double) -> String
let onSave: (Double) -> Void
let onClear: (() -> Void)?
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Form {
Section {
Stepper(value: $value, in: minimum...maximum, step: step) {
Label {
Text(formattedDurationProvider(value))
.font(.title3.weight(.semibold))
} icon: {
Image(systemName: "flag.checkered")
.symbolRenderingMode(.hierarchical)
.foregroundStyle(tint)
}
}
}
if showsClear, let onClear {
Section {
Button(role: .destructive) {
onClear()
dismiss()
} label: {
Text(clearTitle)
}
}
}
}
.tint(tint)
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(cancelTitle) {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button(saveTitle) {
onSave(clampedValue)
dismiss()
}
.disabled(!value.isFinite || value < minimum)
}
}
}
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
}
private var clampedValue: Double {
min(max(value, minimum), maximum)
}
}
#Preview("SystemOverview Populated") {
let system = ElectricalSystem(
name: "12V DC System",
@@ -949,6 +1408,8 @@ struct SystemOverviewView: View {
iconName: "bolt.circle.fill",
colorName: "blue"
)
//system.targetRuntimeHours = 8
//system.targetChargeTimeHours = 6
let loads: [SavedLoad] = [
SavedLoad(
@@ -1023,10 +1484,12 @@ struct SystemOverviewView: View {
onSelectChargers: {},
onCreateLoad: {},
onBrowseLibrary: {},
onShowBillOfMaterials: {},
onCreateBattery: {},
onCreateCharger: {}
)
.padding()
.modelContainer(for: [ElectricalSystem.self, SavedLoad.self, SavedBattery.self, SavedCharger.self], inMemory: true)
}
#Preview("SystemOverview Empty States") {
@@ -1047,8 +1510,10 @@ struct SystemOverviewView: View {
onSelectChargers: {},
onCreateLoad: {},
onBrowseLibrary: {},
onShowBillOfMaterials: {},
onCreateBattery: {},
onCreateCharger: {}
)
.padding()
.modelContainer(for: [ElectricalSystem.self, SavedLoad.self, SavedBattery.self, SavedCharger.self], inMemory: true)
}

View File

@@ -240,6 +240,22 @@
"overview.runtime.title" = "Geschätzte Laufzeit";
"overview.runtime.unavailable" = "Trage Batteriekapazität und Leistungsaufnahme ein, um die Laufzeit zu schätzen.";
"overview.system.header.title" = "Systemübersicht";
"overview.bom.title" = "Stückliste";
"overview.bom.subtitle" = "Tippe, um Komponenten zu prüfen";
"overview.bom.unavailable" = "Füge Verbraucher hinzu, um Komponenten zu erzeugen.";
"overview.bom.placeholder.short" = "Verbraucher hinzufügen";
"overview.chargetime.title" = "Geschätzte Ladezeit";
"overview.chargetime.subtitle" = "Bei kombinierter Laderate";
"overview.chargetime.unavailable" = "Füge Ladegeräte und Batteriekapazität hinzu, um eine Schätzung zu erhalten.";
"overview.chargetime.placeholder.short" = "Ladegeräte hinzufügen";
"overview.goal.prefix" = "Ziel";
"overview.goal.label" = "Ziel %@";
"overview.goal.clear" = "Ziel entfernen";
"overview.goal.cancel" = "Abbrechen";
"overview.goal.save" = "Speichern";
"overview.runtime.goal.title" = "Laufzeit-Ziel";
"overview.chargetime.goal.title" = "Ladezeit-Ziel";
"overview.runtime.placeholder.short" = "Kapazität hinzufügen";
"sample.battery.rv.name" = "LiFePO4-Bordbatterie";
"sample.battery.workshop.name" = "Werkbank-Reservebatterie";
"sample.charger.dcdc.name" = "DC-DC-Ladegerät";

View File

@@ -176,6 +176,22 @@
"overview.runtime.title" = "Autonomía estimada";
"overview.runtime.subtitle" = "Con la carga actual";
"overview.runtime.unavailable" = "Añade capacidad de batería y potencia de carga para estimar la autonomía.";
"overview.bom.title" = "Lista de materiales";
"overview.bom.subtitle" = "Pulsa para revisar los componentes";
"overview.bom.unavailable" = "Añade cargas para generar componentes.";
"overview.bom.placeholder.short" = "Añadir cargas";
"overview.chargetime.title" = "Tiempo de carga estimado";
"overview.chargetime.subtitle" = "Con la tasa de carga combinada";
"overview.chargetime.unavailable" = "Añade cargadores y capacidad de batería para calcularlo.";
"overview.chargetime.placeholder.short" = "Añadir cargadores";
"overview.goal.prefix" = "Objetivo";
"overview.goal.label" = "Objetivo %@";
"overview.goal.clear" = "Eliminar objetivo";
"overview.goal.cancel" = "Cancelar";
"overview.goal.save" = "Guardar";
"overview.runtime.goal.title" = "Objetivo de autonomía";
"overview.chargetime.goal.title" = "Objetivo de carga";
"overview.runtime.placeholder.short" = "Añadir capacidad";
"battery.bank.warning.voltage.short" = "Voltaje";
"battery.bank.warning.capacity.short" = "Capacidad";

View File

@@ -176,6 +176,22 @@
"overview.runtime.title" = "Autonomie estimée";
"overview.runtime.subtitle" = "Avec la charge actuelle";
"overview.runtime.unavailable" = "Ajoutez la capacité de la batterie et la puissance consommée pour estimer lautonomie.";
"overview.bom.title" = "Liste de matériel";
"overview.bom.subtitle" = "Touchez pour consulter les composants";
"overview.bom.unavailable" = "Ajoutez des charges pour générer des composants.";
"overview.bom.placeholder.short" = "Ajouter des charges";
"overview.chargetime.title" = "Temps de charge estimé";
"overview.chargetime.subtitle" = "Au débit de charge combiné";
"overview.chargetime.unavailable" = "Ajoutez des chargeurs et de la capacité batterie pour estimer.";
"overview.chargetime.placeholder.short" = "Ajouter des chargeurs";
"overview.goal.prefix" = "Objectif";
"overview.goal.label" = "Objectif %@";
"overview.goal.clear" = "Supprimer l'objectif";
"overview.goal.cancel" = "Annuler";
"overview.goal.save" = "Enregistrer";
"overview.runtime.goal.title" = "Objectif d'autonomie";
"overview.chargetime.goal.title" = "Objectif de recharge";
"overview.runtime.placeholder.short" = "Ajouter capacité";
"battery.bank.warning.voltage.short" = "Tension";
"battery.bank.warning.capacity.short" = "Capacité";

View File

@@ -176,6 +176,22 @@
"overview.runtime.title" = "Geschatte looptijd";
"overview.runtime.subtitle" = "Bij huidige belasting";
"overview.runtime.unavailable" = "Voeg batterijcapaciteit en vermogensvraag toe om de looptijd te berekenen.";
"overview.bom.title" = "Stuklijst";
"overview.bom.subtitle" = "Tik om componenten te bekijken";
"overview.bom.unavailable" = "Voeg verbruikers toe om componenten te genereren.";
"overview.bom.placeholder.short" = "Lasten toevoegen";
"overview.chargetime.title" = "Geschatte laadtijd";
"overview.chargetime.subtitle" = "Met gecombineerde laadsnelheid";
"overview.chargetime.unavailable" = "Voeg laders en accucapaciteit toe voor een schatting.";
"overview.chargetime.placeholder.short" = "Laders toevoegen";
"overview.goal.prefix" = "Doel";
"overview.goal.label" = "Doel %@";
"overview.goal.clear" = "Doel verwijderen";
"overview.goal.cancel" = "Annuleren";
"overview.goal.save" = "Opslaan";
"overview.runtime.goal.title" = "Looptijddoel";
"overview.chargetime.goal.title" = "Laadtijddoel";
"overview.runtime.placeholder.short" = "Capaciteit toevoegen";
"battery.bank.warning.voltage.short" = "Spanning";
"battery.bank.warning.capacity.short" = "Capaciteit";