Compare commits
2 Commits
10dc0e4fa9
...
97a9d3903c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a9d3903c | ||
|
|
45a462295d |
@@ -405,7 +405,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_TEAM = RE4FXQ754N;
|
DEVELOPMENT_TEAM = RE4FXQ754N;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Cable/Cable.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 32;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_TEAM = RE4FXQ754N;
|
DEVELOPMENT_TEAM = RE4FXQ754N;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|||||||
@@ -180,6 +180,24 @@
|
|||||||
"overview.runtime.title" = "Estimated runtime";
|
"overview.runtime.title" = "Estimated runtime";
|
||||||
"overview.runtime.unavailable" = "Add battery capacity and load power to estimate runtime.";
|
"overview.runtime.unavailable" = "Add battery capacity and load power to estimate runtime.";
|
||||||
"overview.system.header.title" = "System Overview";
|
"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";
|
"sample.charger.dcdc.name" = "DC-DC charger";
|
||||||
"sample.charger.shore.name" = "Shore power charger";
|
"sample.charger.shore.name" = "Shore power charger";
|
||||||
"sample.charger.workbench.name" = "Workbench charger";
|
"sample.charger.workbench.name" = "Workbench charger";
|
||||||
|
|||||||
@@ -112,13 +112,24 @@ class ElectricalSystem {
|
|||||||
var timestamp: Date = Date()
|
var timestamp: Date = Date()
|
||||||
var iconName: String = "building.2"
|
var iconName: String = "building.2"
|
||||||
var colorName: String = "blue"
|
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.name = name
|
||||||
self.location = location
|
self.location = location
|
||||||
self.timestamp = Date()
|
self.timestamp = Date()
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.colorName = colorName
|
self.colorName = colorName
|
||||||
|
self.targetRuntimeHours = targetRuntimeHours
|
||||||
|
self.targetChargeTimeHours = targetChargeTimeHours
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ struct LoadsView: View {
|
|||||||
onSelectChargers: { selectedComponentTab = .chargers },
|
onSelectChargers: { selectedComponentTab = .chargers },
|
||||||
onCreateLoad: { createNewLoad() },
|
onCreateLoad: { createNewLoad() },
|
||||||
onBrowseLibrary: { showingComponentLibrary = true },
|
onBrowseLibrary: { showingComponentLibrary = true },
|
||||||
|
onShowBillOfMaterials: { showingSystemBOM = true },
|
||||||
onCreateBattery: { startBatteryConfiguration() },
|
onCreateBattery: { startBatteryConfiguration() },
|
||||||
onCreateCharger: { startChargerConfiguration() }
|
onCreateCharger: { startChargerConfiguration() }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ struct OnboardingInfoView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 12) {
|
VStack(spacing: 12) {
|
||||||
|
Spacer()
|
||||||
Button(action: onPrimaryAction) {
|
Button(action: onPrimaryAction) {
|
||||||
Label(configuration.primaryActionTitle, systemImage: configuration.primaryActionIcon)
|
Label(configuration.primaryActionTitle, systemImage: configuration.primaryActionIcon)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@@ -76,6 +77,7 @@ struct OnboardingInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
.frame(minHeight: 140)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 24)
|
.padding(.vertical, 24)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
struct SystemOverviewView: View {
|
struct SystemOverviewView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@Environment(\.modelContext) private var modelContext
|
||||||
@State private var activeStatus: LoadConfigurationStatus?
|
@State private var activeStatus: LoadConfigurationStatus?
|
||||||
@State private var suppressLoadNavigation = false
|
@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 system: ElectricalSystem
|
||||||
let loads: [SavedLoad]
|
let loads: [SavedLoad]
|
||||||
let batteries: [SavedBattery]
|
let batteries: [SavedBattery]
|
||||||
@@ -13,108 +19,205 @@ struct SystemOverviewView: View {
|
|||||||
let onSelectChargers: () -> Void
|
let onSelectChargers: () -> Void
|
||||||
let onCreateLoad: () -> Void
|
let onCreateLoad: () -> Void
|
||||||
let onBrowseLibrary: () -> Void
|
let onBrowseLibrary: () -> Void
|
||||||
|
let onShowBillOfMaterials: () -> Void
|
||||||
let onCreateBattery: () -> Void
|
let onCreateBattery: () -> Void
|
||||||
let onCreateCharger: () -> Void
|
let onCreateCharger: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
VStack(spacing: 0) {
|
||||||
VStack(spacing: 16) {
|
systemCard
|
||||||
systemCard
|
.padding(.horizontal, 16)
|
||||||
loadsCard
|
.padding(.top, 20)
|
||||||
batteriesCard
|
.padding(.bottom, 16)
|
||||||
chargersCard
|
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
loadsCard
|
||||||
|
batteriesCard
|
||||||
|
chargersCard
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.bottom, 20)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 20)
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.background(Color(.systemGroupedBackground))
|
.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 {
|
private var systemCard: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 18) {
|
||||||
Text(systemOverviewTitle)
|
Text(systemOverviewTitle)
|
||||||
.font(.headline.weight(.semibold))
|
.font(.headline.weight(.semibold))
|
||||||
HStack(spacing: 14) {
|
|
||||||
ZStack {
|
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
|
||||||
.fill(Color.componentColor(named: system.colorName))
|
|
||||||
.frame(width: 54, height: 54)
|
|
||||||
Image(systemName: system.iconName)
|
|
||||||
.font(.system(size: 24, weight: .semibold))
|
|
||||||
.foregroundStyle(Color.white)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(spacing: 14) {
|
||||||
Text(system.name)
|
overviewMetricRow(
|
||||||
.font(.title3.weight(.semibold))
|
title: runtimeTitle,
|
||||||
.lineLimit(2)
|
subtitle: runtimeSubtitle,
|
||||||
.multilineTextAlignment(.leading)
|
icon: "clock.arrow.circlepath",
|
||||||
if !system.location.isEmpty {
|
tint: .orange,
|
||||||
Label(system.location, systemImage: "mappin.and.ellipse")
|
value: formattedRuntime,
|
||||||
.font(.subheadline)
|
placeholder: runtimePlaceholderSummary,
|
||||||
.foregroundStyle(.secondary)
|
goal: runtimeGoalValueText,
|
||||||
.labelStyle(.titleAndIcon)
|
actualHours: estimatedRuntimeHours,
|
||||||
}
|
goalHours: system.targetRuntimeHours,
|
||||||
}
|
progressFraction: nil,
|
||||||
Spacer()
|
hasValue: formattedRuntime != nil,
|
||||||
|
action: openRuntimeGoalEditor
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
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(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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runtimeSection
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
|
||||||
.padding(.vertical, 18)
|
.padding(.vertical, 18)
|
||||||
|
.padding(.horizontal, 20)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
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
|
@ViewBuilder
|
||||||
private var loadsCard: some View {
|
private var loadsCard: some View {
|
||||||
if loads.isEmpty {
|
if loads.isEmpty {
|
||||||
@@ -290,7 +393,6 @@ struct SystemOverviewView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: onCreateBattery) {
|
Button(action: onCreateBattery) {
|
||||||
Label(batteryEmptyCreateAction, systemImage: "plus")
|
Label(batteryEmptyCreateAction, systemImage: "plus")
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
@@ -555,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 {
|
private func statusBanner(for status: LoadConfigurationStatus) -> some View {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
Image(systemName: status.symbol)
|
Image(systemName: status.symbol)
|
||||||
@@ -604,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 {
|
private func formattedCurrent(_ value: Double) -> String {
|
||||||
let numberString = Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
|
let numberString = Self.numberFormatter.string(from: NSNumber(value: value)) ?? String(format: "%.1f", value)
|
||||||
return "\(numberString) A"
|
return "\(numberString) A"
|
||||||
@@ -614,6 +720,10 @@ struct SystemOverviewView: View {
|
|||||||
return "\(numberString) W"
|
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 {
|
private func formattedChargerOutput(_ value: Double?) -> String {
|
||||||
guard let value, value > 0 else { return "—" }
|
guard let value, value > 0 else { return "—" }
|
||||||
return formattedValue(value, unit: "V")
|
return formattedValue(value, unit: "V")
|
||||||
@@ -624,6 +734,32 @@ struct SystemOverviewView: View {
|
|||||||
return "\(numberString) \(unit)"
|
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? {
|
private var estimatedRuntimeHours: Double? {
|
||||||
guard totalPower > 0, totalUsableEnergy > 0 else { return nil }
|
guard totalPower > 0, totalUsableEnergy > 0 else { return nil }
|
||||||
let hours = totalUsableEnergy / totalPower
|
let hours = totalUsableEnergy / totalPower
|
||||||
@@ -639,9 +775,99 @@ struct SystemOverviewView: View {
|
|||||||
let numberString = Self.numberFormatter.string(from: NSNumber(value: hours)) ?? String(format: "%.1f", hours)
|
let numberString = Self.numberFormatter.string(from: NSNumber(value: hours)) ?? String(format: "%.1f", hours)
|
||||||
return "\(numberString) h"
|
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 {
|
private var estimatedChargeHours: Double? {
|
||||||
!batteries.isEmpty && !loads.isEmpty && formattedRuntime == nil
|
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 {
|
private var loadsSummaryTitle: String {
|
||||||
@@ -880,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 {
|
private var runtimeTitle: String {
|
||||||
NSLocalizedString(
|
NSLocalizedString(
|
||||||
"overview.runtime.title",
|
"overview.runtime.title",
|
||||||
@@ -898,12 +1201,66 @@ struct SystemOverviewView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var runtimeUnavailableText: String {
|
private var runtimePlaceholderSummary: String {
|
||||||
NSLocalizedString(
|
NSLocalizedString(
|
||||||
"overview.runtime.unavailable",
|
"overview.runtime.placeholder.short",
|
||||||
bundle: .main,
|
bundle: .main,
|
||||||
value: "Add battery capacity and load power to estimate runtime.",
|
value: "Add capacity",
|
||||||
comment: "Message shown when runtime cannot be calculated"
|
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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,6 +1280,13 @@ struct SystemOverviewView: View {
|
|||||||
return formatter
|
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 {
|
private enum BatteryWarning {
|
||||||
case voltage(count: Int)
|
case voltage(count: Int)
|
||||||
case capacity(count: Int)
|
case capacity(count: Int)
|
||||||
@@ -966,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") {
|
#Preview("SystemOverview – Populated") {
|
||||||
let system = ElectricalSystem(
|
let system = ElectricalSystem(
|
||||||
name: "12V DC System",
|
name: "12V DC System",
|
||||||
@@ -973,6 +1408,8 @@ struct SystemOverviewView: View {
|
|||||||
iconName: "bolt.circle.fill",
|
iconName: "bolt.circle.fill",
|
||||||
colorName: "blue"
|
colorName: "blue"
|
||||||
)
|
)
|
||||||
|
//system.targetRuntimeHours = 8
|
||||||
|
//system.targetChargeTimeHours = 6
|
||||||
|
|
||||||
let loads: [SavedLoad] = [
|
let loads: [SavedLoad] = [
|
||||||
SavedLoad(
|
SavedLoad(
|
||||||
@@ -1047,10 +1484,12 @@ struct SystemOverviewView: View {
|
|||||||
onSelectChargers: {},
|
onSelectChargers: {},
|
||||||
onCreateLoad: {},
|
onCreateLoad: {},
|
||||||
onBrowseLibrary: {},
|
onBrowseLibrary: {},
|
||||||
|
onShowBillOfMaterials: {},
|
||||||
onCreateBattery: {},
|
onCreateBattery: {},
|
||||||
onCreateCharger: {}
|
onCreateCharger: {}
|
||||||
)
|
)
|
||||||
.padding()
|
.padding()
|
||||||
|
.modelContainer(for: [ElectricalSystem.self, SavedLoad.self, SavedBattery.self, SavedCharger.self], inMemory: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("SystemOverview – Empty States") {
|
#Preview("SystemOverview – Empty States") {
|
||||||
@@ -1071,8 +1510,10 @@ struct SystemOverviewView: View {
|
|||||||
onSelectChargers: {},
|
onSelectChargers: {},
|
||||||
onCreateLoad: {},
|
onCreateLoad: {},
|
||||||
onBrowseLibrary: {},
|
onBrowseLibrary: {},
|
||||||
|
onShowBillOfMaterials: {},
|
||||||
onCreateBattery: {},
|
onCreateBattery: {},
|
||||||
onCreateCharger: {}
|
onCreateCharger: {}
|
||||||
)
|
)
|
||||||
.padding()
|
.padding()
|
||||||
|
.modelContainer(for: [ElectricalSystem.self, SavedLoad.self, SavedBattery.self, SavedCharger.self], inMemory: true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,38 @@ private extension UITestSampleData {
|
|||||||
|
|
||||||
[vanFridge, vanLighting, workshopCompressor, workshopCharger].forEach { context.insert($0) }
|
[vanFridge, vanLighting, workshopCompressor, workshopCharger].forEach { context.insert($0) }
|
||||||
|
|
||||||
|
let vanHouseBattery = SavedBattery(
|
||||||
|
name: String(localized: "sample.battery.rv.name", comment: "Sample data battery name for the adventure van system"),
|
||||||
|
nominalVoltage: 12.8,
|
||||||
|
capacityAmpHours: 200.0,
|
||||||
|
chemistry: .lithiumIronPhosphate,
|
||||||
|
chargeVoltage: 14.4,
|
||||||
|
cutOffVoltage: 10.8,
|
||||||
|
minimumTemperatureCelsius: -20,
|
||||||
|
maximumTemperatureCelsius: 60,
|
||||||
|
iconName: "battery.100.bolt",
|
||||||
|
colorName: "purple",
|
||||||
|
system: adventureVan
|
||||||
|
)
|
||||||
|
vanHouseBattery.timestamp = Date(timeIntervalSinceReferenceDate: 1250)
|
||||||
|
|
||||||
|
let workshopBackupBattery = SavedBattery(
|
||||||
|
name: String(localized: "sample.battery.workshop.name", comment: "Sample data battery name for the workshop system"),
|
||||||
|
nominalVoltage: 24.0,
|
||||||
|
capacityAmpHours: 100.0,
|
||||||
|
chemistry: .agm,
|
||||||
|
chargeVoltage: 28.8,
|
||||||
|
cutOffVoltage: 21.0,
|
||||||
|
minimumTemperatureCelsius: -10,
|
||||||
|
maximumTemperatureCelsius: 50,
|
||||||
|
iconName: "battery.75",
|
||||||
|
colorName: "gray",
|
||||||
|
system: workshopBench
|
||||||
|
)
|
||||||
|
workshopBackupBattery.timestamp = Date(timeIntervalSinceReferenceDate: 2300)
|
||||||
|
|
||||||
|
[vanHouseBattery, workshopBackupBattery].forEach { context.insert($0) }
|
||||||
|
|
||||||
let shoreCharger = SavedCharger(
|
let shoreCharger = SavedCharger(
|
||||||
name: String(localized: "sample.charger.shore.name", comment: "Sample data name for a shore power charger"),
|
name: String(localized: "sample.charger.shore.name", comment: "Sample data name for a shore power charger"),
|
||||||
inputVoltage: 230.0,
|
inputVoltage: 230.0,
|
||||||
|
|||||||
@@ -217,7 +217,7 @@
|
|||||||
"loads.metric.fuse" = "Sicherung";
|
"loads.metric.fuse" = "Sicherung";
|
||||||
"loads.metric.length" = "Länge";
|
"loads.metric.length" = "Länge";
|
||||||
"loads.onboarding.subtitle" = "Statte dein System mit Verbrauchern aus und lass **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen übernehmen.";
|
"loads.onboarding.subtitle" = "Statte dein System mit Verbrauchern aus und lass **Cable by VoltPlan** die Kabel- und Sicherungsempfehlungen übernehmen.";
|
||||||
"loads.onboarding.title" = "Füge deinen ersten Verbraucher hinzu";
|
"loads.onboarding.title" = "Erstelle deinen ersten Verbraucher";
|
||||||
"loads.overview.empty.create" = "Verbraucher hinzufügen";
|
"loads.overview.empty.create" = "Verbraucher hinzufügen";
|
||||||
"loads.overview.empty.library" = "Bibliothek durchsuchen";
|
"loads.overview.empty.library" = "Bibliothek durchsuchen";
|
||||||
"loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten.";
|
"loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten.";
|
||||||
@@ -240,6 +240,24 @@
|
|||||||
"overview.runtime.title" = "Geschätzte Laufzeit";
|
"overview.runtime.title" = "Geschätzte Laufzeit";
|
||||||
"overview.runtime.unavailable" = "Trage Batteriekapazität und Leistungsaufnahme ein, um die Laufzeit zu schätzen.";
|
"overview.runtime.unavailable" = "Trage Batteriekapazität und Leistungsaufnahme ein, um die Laufzeit zu schätzen.";
|
||||||
"overview.system.header.title" = "Systemübersicht";
|
"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";
|
"sample.charger.dcdc.name" = "DC-DC-Ladegerät";
|
||||||
"sample.charger.shore.name" = "Landstrom-Ladegerät";
|
"sample.charger.shore.name" = "Landstrom-Ladegerät";
|
||||||
"sample.charger.workbench.name" = "Werkbank-Ladegerät";
|
"sample.charger.workbench.name" = "Werkbank-Ladegerät";
|
||||||
|
|||||||
@@ -176,6 +176,22 @@
|
|||||||
"overview.runtime.title" = "Autonomía estimada";
|
"overview.runtime.title" = "Autonomía estimada";
|
||||||
"overview.runtime.subtitle" = "Con la carga actual";
|
"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.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.voltage.short" = "Voltaje";
|
||||||
"battery.bank.warning.capacity.short" = "Capacidad";
|
"battery.bank.warning.capacity.short" = "Capacidad";
|
||||||
|
|
||||||
@@ -293,6 +309,8 @@
|
|||||||
"chargers.onboarding.subtitle" = "Lleva el control del cargador de costa, los boosters y los controladores solares para saber cuánta potencia de carga tienes.";
|
"chargers.onboarding.subtitle" = "Lleva el control del cargador de costa, los boosters y los controladores solares para saber cuánta potencia de carga tienes.";
|
||||||
"chargers.onboarding.primary" = "Crear cargador";
|
"chargers.onboarding.primary" = "Crear cargador";
|
||||||
|
|
||||||
|
"sample.battery.rv.name" = "Banco LiFePO4 de servicio";
|
||||||
|
"sample.battery.workshop.name" = "Batería de respaldo del banco de trabajo";
|
||||||
"sample.charger.shore.name" = "Cargador de costa";
|
"sample.charger.shore.name" = "Cargador de costa";
|
||||||
"sample.charger.dcdc.name" = "Cargador DC-DC";
|
"sample.charger.dcdc.name" = "Cargador DC-DC";
|
||||||
"sample.charger.workbench.name" = "Cargador de banco de trabajo";
|
"sample.charger.workbench.name" = "Cargador de banco de trabajo";
|
||||||
|
|||||||
@@ -176,6 +176,22 @@
|
|||||||
"overview.runtime.title" = "Autonomie estimée";
|
"overview.runtime.title" = "Autonomie estimée";
|
||||||
"overview.runtime.subtitle" = "Avec la charge actuelle";
|
"overview.runtime.subtitle" = "Avec la charge actuelle";
|
||||||
"overview.runtime.unavailable" = "Ajoutez la capacité de la batterie et la puissance consommée pour estimer l’autonomie.";
|
"overview.runtime.unavailable" = "Ajoutez la capacité de la batterie et la puissance consommée pour estimer l’autonomie.";
|
||||||
|
"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.voltage.short" = "Tension";
|
||||||
"battery.bank.warning.capacity.short" = "Capacité";
|
"battery.bank.warning.capacity.short" = "Capacité";
|
||||||
|
|
||||||
@@ -293,6 +309,8 @@
|
|||||||
"chargers.onboarding.subtitle" = "Suivez l'alimentation quai, les boosters et les régulateurs solaires pour connaître votre capacité de charge.";
|
"chargers.onboarding.subtitle" = "Suivez l'alimentation quai, les boosters et les régulateurs solaires pour connaître votre capacité de charge.";
|
||||||
"chargers.onboarding.primary" = "Créer un chargeur";
|
"chargers.onboarding.primary" = "Créer un chargeur";
|
||||||
|
|
||||||
|
"sample.battery.rv.name" = "Batterie de service LiFePO4";
|
||||||
|
"sample.battery.workshop.name" = "Batterie de secours de l'établi";
|
||||||
"sample.charger.shore.name" = "Chargeur de quai";
|
"sample.charger.shore.name" = "Chargeur de quai";
|
||||||
"sample.charger.dcdc.name" = "Chargeur DC-DC";
|
"sample.charger.dcdc.name" = "Chargeur DC-DC";
|
||||||
"sample.charger.workbench.name" = "Chargeur d'établi";
|
"sample.charger.workbench.name" = "Chargeur d'établi";
|
||||||
|
|||||||
@@ -176,6 +176,22 @@
|
|||||||
"overview.runtime.title" = "Geschatte looptijd";
|
"overview.runtime.title" = "Geschatte looptijd";
|
||||||
"overview.runtime.subtitle" = "Bij huidige belasting";
|
"overview.runtime.subtitle" = "Bij huidige belasting";
|
||||||
"overview.runtime.unavailable" = "Voeg batterijcapaciteit en vermogensvraag toe om de looptijd te berekenen.";
|
"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.voltage.short" = "Spanning";
|
||||||
"battery.bank.warning.capacity.short" = "Capaciteit";
|
"battery.bank.warning.capacity.short" = "Capaciteit";
|
||||||
|
|
||||||
@@ -293,6 +309,8 @@
|
|||||||
"chargers.onboarding.subtitle" = "Houd walstroom, boosters en zonne-regelaars bij om je laadcapaciteit te kennen.";
|
"chargers.onboarding.subtitle" = "Houd walstroom, boosters en zonne-regelaars bij om je laadcapaciteit te kennen.";
|
||||||
"chargers.onboarding.primary" = "Lader aanmaken";
|
"chargers.onboarding.primary" = "Lader aanmaken";
|
||||||
|
|
||||||
|
"sample.battery.rv.name" = "LiFePO4-huishoudaccu";
|
||||||
|
"sample.battery.workshop.name" = "Reserveaccu voor werkbank";
|
||||||
"sample.charger.shore.name" = "Walstroomlader";
|
"sample.charger.shore.name" = "Walstroomlader";
|
||||||
"sample.charger.dcdc.name" = "DC-DC-lader";
|
"sample.charger.dcdc.name" = "DC-DC-lader";
|
||||||
"sample.charger.workbench.name" = "Werkplaatslader";
|
"sample.charger.workbench.name" = "Werkplaatslader";
|
||||||
|
|||||||
Reference in New Issue
Block a user