adds chargers
This commit is contained in:
@@ -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 = 14;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = RE4FXQ754N;
|
DEVELOPMENT_TEAM = RE4FXQ754N;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -422,7 +422,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
|
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -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 = 14;
|
CURRENT_PROJECT_VERSION = 28;
|
||||||
DEVELOPMENT_TEAM = RE4FXQ754N;
|
DEVELOPMENT_TEAM = RE4FXQ754N;
|
||||||
ENABLE_APP_SANDBOX = YES;
|
ENABLE_APP_SANDBOX = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -457,7 +457,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.1;
|
MARKETING_VERSION = 1.5;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
|
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
52
Cable/Assets.xcassets/battery-onboarding.imageset/Contents.json
vendored
Normal file
52
Cable/Assets.xcassets/battery-onboarding.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "battery-light.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"filename" : "battery-dark.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Cable/Assets.xcassets/battery-onboarding.imageset/battery-dark.png
vendored
Normal file
BIN
Cable/Assets.xcassets/battery-onboarding.imageset/battery-dark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
Cable/Assets.xcassets/battery-onboarding.imageset/battery-light.png
vendored
Normal file
BIN
Cable/Assets.xcassets/battery-onboarding.imageset/battery-light.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
@@ -141,5 +141,38 @@
|
|||||||
"battery.editor.alert.save" = "Save";
|
"battery.editor.alert.save" = "Save";
|
||||||
"battery.editor.default_name" = "New Battery";
|
"battery.editor.default_name" = "New Battery";
|
||||||
|
|
||||||
|
"charger.editor.title" = "Charger";
|
||||||
|
"charger.editor.field.name" = "Name";
|
||||||
|
"charger.editor.placeholder.name" = "Workshop Charger";
|
||||||
|
"charger.editor.section.electrical" = "Electrical";
|
||||||
|
"charger.editor.section.power" = "Charge Output";
|
||||||
|
"charger.editor.appearance.title" = "Charger Appearance";
|
||||||
|
"charger.editor.appearance.subtitle" = "Customize how this charger shows up";
|
||||||
|
"charger.editor.appearance.accessibility" = "Edit charger appearance";
|
||||||
|
"charger.editor.field.input_voltage" = "Input Voltage";
|
||||||
|
"charger.editor.field.output_voltage" = "Output Voltage";
|
||||||
|
"charger.editor.field.current" = "Charge Current";
|
||||||
|
"charger.editor.field.power" = "Charge Power";
|
||||||
|
"charger.editor.field.power.footer" = "Leave blank when the rated wattage isn't published. We'll calculate it from voltage and current.";
|
||||||
|
"charger.editor.default_name" = "New Charger";
|
||||||
|
"charger.default.new" = "New Charger";
|
||||||
|
|
||||||
|
"chargers.summary.title" = "Charging Overview";
|
||||||
|
"chargers.summary.metric.count" = "Chargers";
|
||||||
|
"chargers.summary.metric.output" = "Output Voltage";
|
||||||
|
"chargers.summary.metric.current" = "Charge Rate";
|
||||||
|
"chargers.summary.metric.power" = "Charge Power";
|
||||||
|
"chargers.badge.input" = "Input";
|
||||||
|
"chargers.badge.output" = "Output";
|
||||||
|
"chargers.badge.current" = "Current";
|
||||||
|
"chargers.badge.power" = "Power";
|
||||||
|
"chargers.onboarding.title" = "Add your chargers";
|
||||||
|
"chargers.onboarding.subtitle" = "Track shore power supplies, alternator chargers, and solar controllers to understand your charging capacity.";
|
||||||
|
"chargers.onboarding.primary" = "Create Charger";
|
||||||
|
|
||||||
|
"sample.charger.shore.name" = "Shore power charger";
|
||||||
|
"sample.charger.dcdc.name" = "DC-DC charger";
|
||||||
|
"sample.charger.workbench.name" = "Workbench charger";
|
||||||
|
|
||||||
"chargers.title" = "Chargers for %@";
|
"chargers.title" = "Chargers for %@";
|
||||||
"chargers.subtitle" = "Charger components will be available soon.";
|
"chargers.subtitle" = "Charger components will be available soon.";
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ struct BatteriesView: View {
|
|||||||
private func batteryIcon(for battery: SavedBattery) -> some View {
|
private func batteryIcon(for battery: SavedBattery) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.fill(colorForName(battery.colorName))
|
.fill(Color.componentColor(named: battery.colorName))
|
||||||
.frame(width: 48, height: 48)
|
.frame(width: 48, height: 48)
|
||||||
Image(systemName: battery.iconName.isEmpty ? "battery.100.bolt" : battery.iconName)
|
Image(systemName: battery.iconName.isEmpty ? "battery.100.bolt" : battery.iconName)
|
||||||
.font(.system(size: 22, weight: .semibold))
|
.font(.system(size: 22, weight: .semibold))
|
||||||
@@ -336,36 +336,19 @@ struct BatteriesView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
|
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
ComponentSummaryMetricView(
|
||||||
HStack(spacing: 6) {
|
icon: icon,
|
||||||
Image(systemName: icon)
|
label: label,
|
||||||
.font(.system(size: 14, weight: .semibold))
|
value: value,
|
||||||
.foregroundStyle(tint)
|
tint: tint
|
||||||
Text(value)
|
)
|
||||||
.font(.body.weight(.semibold))
|
|
||||||
}
|
|
||||||
Text(label.uppercased())
|
|
||||||
.font(.caption2)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func metricBadge(label: String, value: String, tint: Color) -> some View {
|
private func metricBadge(label: String, value: String, tint: Color) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
ComponentMetricBadgeView(
|
||||||
Text(label.uppercased())
|
label: label,
|
||||||
.font(.caption2)
|
value: value,
|
||||||
.fontWeight(.medium)
|
tint: tint
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Text(value)
|
|
||||||
.font(.subheadline.weight(.semibold))
|
|
||||||
.foregroundStyle(tint)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
||||||
.fill(tint.opacity(0.12))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,25 +370,6 @@ struct BatteriesView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorForName(_ colorName: String) -> Color {
|
|
||||||
switch colorName {
|
|
||||||
case "blue": return .blue
|
|
||||||
case "green": return .green
|
|
||||||
case "orange": return .orange
|
|
||||||
case "red": return .red
|
|
||||||
case "purple": return .purple
|
|
||||||
case "yellow": return .yellow
|
|
||||||
case "pink": return .pink
|
|
||||||
case "teal": return .teal
|
|
||||||
case "indigo": return .indigo
|
|
||||||
case "mint": return .mint
|
|
||||||
case "cyan": return .cyan
|
|
||||||
case "brown": return .brown
|
|
||||||
case "gray": return .gray
|
|
||||||
default: return .blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var emptyState: some View {
|
private var emptyState: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
Image(systemName: "battery.100")
|
Image(systemName: "battery.100")
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ struct BatteryEditorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var iconColor: Color {
|
private var iconColor: Color {
|
||||||
colorForName(configuration.colorName)
|
Color.componentColor(named: configuration.colorName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var displayName: String {
|
private var displayName: String {
|
||||||
@@ -608,25 +608,6 @@ struct BatteryEditorView: View {
|
|||||||
return abs(closest - value) <= tolerance ? closest : nil
|
return abs(closest - value) <= tolerance ? closest : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorForName(_ colorName: String) -> Color {
|
|
||||||
switch colorName {
|
|
||||||
case "blue": return .blue
|
|
||||||
case "green": return .green
|
|
||||||
case "orange": return .orange
|
|
||||||
case "red": return .red
|
|
||||||
case "purple": return .purple
|
|
||||||
case "yellow": return .yellow
|
|
||||||
case "pink": return .pink
|
|
||||||
case "teal": return .teal
|
|
||||||
case "indigo": return .indigo
|
|
||||||
case "mint": return .mint
|
|
||||||
case "cyan": return .cyan
|
|
||||||
case "brown": return .brown
|
|
||||||
case "gray": return .gray
|
|
||||||
default: return .blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func summaryBadge(title: String, value: String, symbol: String) -> some View {
|
private func summaryBadge(title: String, value: String, symbol: String) -> some View {
|
||||||
VStack(spacing: 4) {
|
VStack(spacing: 4) {
|
||||||
Image(systemName: symbol)
|
Image(systemName: symbol)
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ struct CableApp: App {
|
|||||||
var sharedModelContainer: ModelContainer = {
|
var sharedModelContainer: ModelContainer = {
|
||||||
do {
|
do {
|
||||||
// Try the simple approach first
|
// Try the simple approach first
|
||||||
return try ModelContainer(for: ElectricalSystem.self, SavedLoad.self, SavedBattery.self, Item.self)
|
return try ModelContainer(for: ElectricalSystem.self, SavedLoad.self, SavedBattery.self, SavedCharger.self, Item.self)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to create ModelContainer with simple approach: \(error)")
|
print("Failed to create ModelContainer with simple approach: \(error)")
|
||||||
|
|
||||||
// Try in-memory as fallback
|
// Try in-memory as fallback
|
||||||
do {
|
do {
|
||||||
let schema = Schema([ElectricalSystem.self, SavedLoad.self, SavedBattery.self, Item.self])
|
let schema = Schema([ElectricalSystem.self, SavedLoad.self, SavedBattery.self, SavedCharger.self, Item.self])
|
||||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
67
Cable/Chargers/ChargerConfiguration.swift
Normal file
67
Cable/Chargers/ChargerConfiguration.swift
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
struct ChargerConfiguration: Identifiable, Hashable {
|
||||||
|
let id: UUID
|
||||||
|
var name: String
|
||||||
|
var inputVoltage: Double
|
||||||
|
var outputVoltage: Double
|
||||||
|
var maxCurrentAmps: Double
|
||||||
|
var maxPowerWatts: Double
|
||||||
|
var iconName: String
|
||||||
|
var colorName: String
|
||||||
|
var system: ElectricalSystem
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID = UUID(),
|
||||||
|
name: String,
|
||||||
|
inputVoltage: Double = 230.0,
|
||||||
|
outputVoltage: Double = 14.2,
|
||||||
|
maxCurrentAmps: Double = 30.0,
|
||||||
|
maxPowerWatts: Double = 0.0,
|
||||||
|
iconName: String = "bolt.fill",
|
||||||
|
colorName: String = "orange",
|
||||||
|
system: ElectricalSystem
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.inputVoltage = inputVoltage
|
||||||
|
self.outputVoltage = outputVoltage
|
||||||
|
self.maxCurrentAmps = maxCurrentAmps
|
||||||
|
self.maxPowerWatts = maxPowerWatts
|
||||||
|
self.iconName = iconName
|
||||||
|
self.colorName = colorName
|
||||||
|
self.system = system
|
||||||
|
}
|
||||||
|
|
||||||
|
init(savedCharger: SavedCharger, system: ElectricalSystem) {
|
||||||
|
self.id = savedCharger.id
|
||||||
|
self.name = savedCharger.name
|
||||||
|
self.inputVoltage = savedCharger.inputVoltage
|
||||||
|
self.outputVoltage = savedCharger.outputVoltage
|
||||||
|
self.maxCurrentAmps = savedCharger.maxCurrentAmps
|
||||||
|
self.maxPowerWatts = savedCharger.maxPowerWatts
|
||||||
|
self.iconName = savedCharger.iconName
|
||||||
|
self.colorName = savedCharger.colorName
|
||||||
|
self.system = system
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectivePowerWatts: Double {
|
||||||
|
if maxPowerWatts > 0 {
|
||||||
|
return maxPowerWatts
|
||||||
|
}
|
||||||
|
return outputVoltage * maxCurrentAmps
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(to savedCharger: SavedCharger) {
|
||||||
|
savedCharger.name = name
|
||||||
|
savedCharger.inputVoltage = inputVoltage
|
||||||
|
savedCharger.outputVoltage = outputVoltage
|
||||||
|
savedCharger.maxCurrentAmps = maxCurrentAmps
|
||||||
|
savedCharger.maxPowerWatts = maxPowerWatts
|
||||||
|
savedCharger.iconName = iconName
|
||||||
|
savedCharger.colorName = colorName
|
||||||
|
savedCharger.system = system
|
||||||
|
savedCharger.timestamp = Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
346
Cable/Chargers/ChargerEditorView.swift
Normal file
346
Cable/Chargers/ChargerEditorView.swift
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChargerEditorView: View {
|
||||||
|
@State private var configuration: ChargerConfiguration
|
||||||
|
@State private var showingAppearanceEditor = false
|
||||||
|
|
||||||
|
let onSave: (ChargerConfiguration) -> Void
|
||||||
|
|
||||||
|
private let chargerIconOptions: [String] = [
|
||||||
|
"bolt.fill",
|
||||||
|
"bolt.circle",
|
||||||
|
"bolt.square",
|
||||||
|
"bolt.badge.clock",
|
||||||
|
"bolt.horizontal",
|
||||||
|
"powerplug",
|
||||||
|
"battery.100.bolt",
|
||||||
|
"car.battery",
|
||||||
|
"engine.combustion",
|
||||||
|
"fanblades",
|
||||||
|
"generator"
|
||||||
|
]
|
||||||
|
|
||||||
|
private var editorTitle: String {
|
||||||
|
NSLocalizedString(
|
||||||
|
"charger.editor.title",
|
||||||
|
bundle: .main,
|
||||||
|
value: "Charger",
|
||||||
|
comment: "Navigation bar title for the charger editor"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var nameFieldLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "charger.editor.field.name",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for the charger name text field"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var namePlaceholder: String {
|
||||||
|
String(
|
||||||
|
localized: "charger.editor.placeholder.name",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Placeholder example for the charger name field"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var electricalSectionTitle: String {
|
||||||
|
String(
|
||||||
|
localized: "charger.editor.section.electrical",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Section title for charger electrical configuration"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var powerSectionTitle: String {
|
||||||
|
String(
|
||||||
|
localized: "charger.editor.section.power",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Section title for charger output power configuration"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appearanceEditorTitle: String {
|
||||||
|
NSLocalizedString(
|
||||||
|
"charger.editor.appearance.title",
|
||||||
|
bundle: .main,
|
||||||
|
value: "Charger Appearance",
|
||||||
|
comment: "Title for the charger appearance editor"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appearanceEditorSubtitle: String {
|
||||||
|
NSLocalizedString(
|
||||||
|
"charger.editor.appearance.subtitle",
|
||||||
|
bundle: .main,
|
||||||
|
value: "Customize how this charger shows up",
|
||||||
|
comment: "Subtitle shown in the charger appearance editor preview"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appearanceAccessibilityLabel: String {
|
||||||
|
NSLocalizedString(
|
||||||
|
"charger.editor.appearance.accessibility",
|
||||||
|
bundle: .main,
|
||||||
|
value: "Edit charger appearance",
|
||||||
|
comment: "Accessibility label for the charger appearance editor button"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(configuration: ChargerConfiguration, onSave: @escaping (ChargerConfiguration) -> Void) {
|
||||||
|
_configuration = State(initialValue: configuration)
|
||||||
|
self.onSave = onSave
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
headerInfoBar
|
||||||
|
List {
|
||||||
|
infoSection
|
||||||
|
electricalSection
|
||||||
|
powerSection
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
.scrollIndicators(.hidden)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(Color.clear)
|
||||||
|
}
|
||||||
|
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
Text(editorTitle)
|
||||||
|
.font(.headline.weight(.semibold))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showingAppearanceEditor) {
|
||||||
|
ItemEditorView(
|
||||||
|
title: appearanceEditorTitle,
|
||||||
|
nameFieldLabel: nameFieldLabel,
|
||||||
|
previewSubtitle: appearanceEditorSubtitle,
|
||||||
|
icons: chargerIconOptions,
|
||||||
|
name: Binding(
|
||||||
|
get: { configuration.name },
|
||||||
|
set: { configuration.name = $0 }
|
||||||
|
),
|
||||||
|
iconName: Binding(
|
||||||
|
get: { configuration.iconName },
|
||||||
|
set: { configuration.iconName = $0 }
|
||||||
|
),
|
||||||
|
colorName: Binding(
|
||||||
|
get: { configuration.colorName },
|
||||||
|
set: { configuration.colorName = $0 }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
onSave(configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var headerInfoBar: some View {
|
||||||
|
HStack(alignment: .center, spacing: 16) {
|
||||||
|
LoadIconView(
|
||||||
|
remoteIconURLString: nil,
|
||||||
|
fallbackSystemName: configuration.iconName,
|
||||||
|
fallbackColor: Color.componentColor(named: configuration.colorName),
|
||||||
|
size: 56
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text(configuration.name.isEmpty ? namePlaceholder : configuration.name)
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
Text(chargerSummary)
|
||||||
|
.font(.footnote.weight(.medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 20)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.background(Color(.systemBackground))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var infoSection: some View {
|
||||||
|
Section {
|
||||||
|
TextField(
|
||||||
|
nameFieldLabel,
|
||||||
|
text: Binding(
|
||||||
|
get: { configuration.name },
|
||||||
|
set: { configuration.name = $0 }
|
||||||
|
),
|
||||||
|
prompt: Text(namePlaceholder)
|
||||||
|
)
|
||||||
|
.textInputAutocapitalization(.words)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
|
||||||
|
Button(action: { showingAppearanceEditor = true }) {
|
||||||
|
Label(
|
||||||
|
appearanceEditorTitle,
|
||||||
|
systemImage: "paintbrush.pointed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.accessibilityLabel(appearanceAccessibilityLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var electricalSection: some View {
|
||||||
|
Section(header: Text(electricalSectionTitle)) {
|
||||||
|
valueRow(
|
||||||
|
title: String(
|
||||||
|
localized: "charger.editor.field.input_voltage",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for the charger input voltage field"
|
||||||
|
),
|
||||||
|
value: $configuration.inputVoltage,
|
||||||
|
unit: "V",
|
||||||
|
range: 0...400,
|
||||||
|
step: 1
|
||||||
|
)
|
||||||
|
|
||||||
|
valueRow(
|
||||||
|
title: String(
|
||||||
|
localized: "charger.editor.field.output_voltage",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for the charger output voltage field"
|
||||||
|
),
|
||||||
|
value: $configuration.outputVoltage,
|
||||||
|
unit: "V",
|
||||||
|
range: 0...80,
|
||||||
|
step: 0.1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var powerSection: some View {
|
||||||
|
Section(header: Text(powerSectionTitle), footer: powerFooterText) {
|
||||||
|
valueRow(
|
||||||
|
title: String(
|
||||||
|
localized: "charger.editor.field.current",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for the charger current field"
|
||||||
|
),
|
||||||
|
value: $configuration.maxCurrentAmps,
|
||||||
|
unit: "A",
|
||||||
|
range: 0...200,
|
||||||
|
step: 0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
valueRow(
|
||||||
|
title: String(
|
||||||
|
localized: "charger.editor.field.power",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for the charger power field"
|
||||||
|
),
|
||||||
|
value: $configuration.maxPowerWatts,
|
||||||
|
unit: "W",
|
||||||
|
range: 0...10000,
|
||||||
|
step: 50,
|
||||||
|
allowsZero: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func valueRow(
|
||||||
|
title: String,
|
||||||
|
value: Binding<Double>,
|
||||||
|
unit: String,
|
||||||
|
range: ClosedRange<Double>,
|
||||||
|
step: Double,
|
||||||
|
allowsZero: Bool = false
|
||||||
|
) -> some View {
|
||||||
|
LabeledContent {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Stepper(
|
||||||
|
value: Binding(
|
||||||
|
get: {
|
||||||
|
clamp(value.wrappedValue, to: range, allowsZero: allowsZero)
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
let clamped = clamp(newValue, to: range, allowsZero: allowsZero)
|
||||||
|
if abs(clamped - value.wrappedValue) > .ulpOfOne {
|
||||||
|
value.wrappedValue = clamped
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
in: range,
|
||||||
|
step: step
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.labelsHidden()
|
||||||
|
TextField(
|
||||||
|
"",
|
||||||
|
value: value,
|
||||||
|
format: .number.precision(.fractionLength(1))
|
||||||
|
)
|
||||||
|
.keyboardType(.decimalPad)
|
||||||
|
.multilineTextAlignment(.trailing)
|
||||||
|
.frame(minWidth: 64)
|
||||||
|
Text(unit)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clamp(_ value: Double, to range: ClosedRange<Double>, allowsZero: Bool) -> Double {
|
||||||
|
if allowsZero && value == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return min(max(value, range.lowerBound), range.upperBound)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var chargerSummary: String {
|
||||||
|
let input = formattedValue(configuration.inputVoltage, unit: "V")
|
||||||
|
let output = formattedValue(configuration.outputVoltage, unit: "V")
|
||||||
|
let current = formattedValue(configuration.maxCurrentAmps, unit: "A")
|
||||||
|
return [input, output, current].joined(separator: " • ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var powerFooterText: Text {
|
||||||
|
Text(
|
||||||
|
String(
|
||||||
|
localized: "charger.editor.field.power.footer",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Footer text explaining power input behaviour"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedValue(_ value: Double, unit: String) -> String {
|
||||||
|
let formatter = NumberFormatter()
|
||||||
|
formatter.numberStyle = .decimal
|
||||||
|
formatter.maximumFractionDigits = unit == "A" ? 1 : 1
|
||||||
|
formatter.minimumFractionDigits = 0
|
||||||
|
guard let formatted = formatter.string(from: value as NSNumber) else {
|
||||||
|
return "\(value) \(unit)"
|
||||||
|
}
|
||||||
|
return "\(formatted)\(unit)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationView {
|
||||||
|
ChargerEditorView(
|
||||||
|
configuration: ChargerConfiguration(
|
||||||
|
name: "Shore Power Charger",
|
||||||
|
inputVoltage: 230,
|
||||||
|
outputVoltage: 14.4,
|
||||||
|
maxCurrentAmps: 40,
|
||||||
|
maxPowerWatts: 600,
|
||||||
|
iconName: "bolt.fill",
|
||||||
|
colorName: "orange",
|
||||||
|
system: ElectricalSystem(name: "Preview System")
|
||||||
|
),
|
||||||
|
onSave: { _ in }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,45 +1,379 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ChargersView: View {
|
struct ChargersView: View {
|
||||||
|
@Binding var editMode: EditMode
|
||||||
let system: ElectricalSystem
|
let system: ElectricalSystem
|
||||||
|
let chargers: [SavedCharger]
|
||||||
|
let onAdd: () -> Void
|
||||||
|
let onEdit: (SavedCharger) -> Void
|
||||||
|
let onDelete: (IndexSet) -> Void
|
||||||
|
|
||||||
private var titleText: String {
|
private struct SummaryMetric: Identifiable {
|
||||||
let format = NSLocalizedString(
|
let id: String
|
||||||
"chargers.title",
|
let icon: String
|
||||||
bundle: .main,
|
let label: String
|
||||||
comment: "Title describing chargers belonging to a system"
|
let value: String
|
||||||
)
|
let tint: Color
|
||||||
return String(format: format, system.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var subtitleText: String {
|
private var summaryTitle: String {
|
||||||
String(
|
String(
|
||||||
localized: "chargers.subtitle",
|
localized: "chargers.summary.title",
|
||||||
bundle: .main,
|
bundle: .main,
|
||||||
comment: "Subtitle shown while chargers tab is under construction"
|
comment: "Title for the chargers summary section"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var summaryCountLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.summary.metric.count",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for number of chargers metric"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var summaryOutputLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.summary.metric.output",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for output voltage metric"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var summaryCurrentLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.summary.metric.current",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for combined current metric"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var summaryPowerLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.summary.metric.power",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for combined power metric"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgeInputLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.badge.input",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for input voltage badge"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgeOutputLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.badge.output",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for output voltage badge"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgeCurrentLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.badge.current",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for charging current badge"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var badgePowerLabel: String {
|
||||||
|
String(
|
||||||
|
localized: "chargers.badge.power",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Label for charging power badge"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
system: ElectricalSystem,
|
||||||
|
chargers: [SavedCharger],
|
||||||
|
editMode: Binding<EditMode> = .constant(.inactive),
|
||||||
|
onAdd: @escaping () -> Void = {},
|
||||||
|
onEdit: @escaping (SavedCharger) -> Void = { _ in },
|
||||||
|
onDelete: @escaping (IndexSet) -> Void = { _ in }
|
||||||
|
) {
|
||||||
|
self.system = system
|
||||||
|
self.chargers = chargers
|
||||||
|
self.onAdd = onAdd
|
||||||
|
self.onEdit = onEdit
|
||||||
|
self.onDelete = onDelete
|
||||||
|
_editMode = editMode
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 0) {
|
||||||
Image(systemName: "bolt.fill")
|
if chargers.isEmpty {
|
||||||
.font(.largeTitle)
|
emptyState
|
||||||
.foregroundStyle(.secondary)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
} else {
|
||||||
|
summarySection
|
||||||
|
|
||||||
Text(titleText)
|
List {
|
||||||
.font(.title3)
|
ForEach(chargers) { charger in
|
||||||
.fontWeight(.semibold)
|
Button {
|
||||||
|
onEdit(charger)
|
||||||
Text(subtitleText)
|
} label: {
|
||||||
.font(.subheadline)
|
chargerRow(for: charger)
|
||||||
.foregroundStyle(.secondary)
|
}
|
||||||
.multilineTextAlignment(.center)
|
.buttonStyle(.plain)
|
||||||
|
.disabled(editMode == .active)
|
||||||
|
.listRowInsets(.init(top: 12, leading: 16, bottom: 12, trailing: 16))
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
}
|
||||||
|
.onDelete(perform: onDelete)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.environment(\.editMode, $editMode)
|
||||||
|
.accessibilityIdentifier("chargers-list")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var summarySection: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
|
Text(summaryTitle)
|
||||||
|
.font(.headline.weight(.semibold))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 16) {
|
||||||
|
ForEach(summaryMetrics) { metric in
|
||||||
|
ComponentSummaryMetricView(
|
||||||
|
icon: metric.icon,
|
||||||
|
label: metric.label,
|
||||||
|
value: metric.value,
|
||||||
|
tint: metric.tint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
}
|
||||||
|
.scrollClipDisabled(true)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
.background(Color(.separator))
|
||||||
|
.padding(.leading, 16)
|
||||||
|
}
|
||||||
|
.background(Color(.systemGroupedBackground))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var summaryMetrics: [SummaryMetric] {
|
||||||
|
guard !chargers.isEmpty else { return [] }
|
||||||
|
|
||||||
|
var metrics: [SummaryMetric] = [
|
||||||
|
SummaryMetric(
|
||||||
|
id: "count",
|
||||||
|
icon: "bolt.fill",
|
||||||
|
label: summaryCountLabel,
|
||||||
|
value: "\(chargers.count)",
|
||||||
|
tint: .blue
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
if let output = representativeOutputVoltage {
|
||||||
|
metrics.append(
|
||||||
|
SummaryMetric(
|
||||||
|
id: "output",
|
||||||
|
icon: "battery.100.bolt",
|
||||||
|
label: summaryOutputLabel,
|
||||||
|
value: formattedVoltage(output),
|
||||||
|
tint: .green
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalCurrent > 0 {
|
||||||
|
metrics.append(
|
||||||
|
SummaryMetric(
|
||||||
|
id: "current",
|
||||||
|
icon: "gauge",
|
||||||
|
label: summaryCurrentLabel,
|
||||||
|
value: formattedCurrent(totalCurrent),
|
||||||
|
tint: .orange
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if totalPower > 0 {
|
||||||
|
metrics.append(
|
||||||
|
SummaryMetric(
|
||||||
|
id: "power",
|
||||||
|
icon: "bolt.badge.a",
|
||||||
|
label: summaryPowerLabel,
|
||||||
|
value: formattedPower(totalPower),
|
||||||
|
tint: .pink
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emptyState: some View {
|
||||||
|
OnboardingInfoView(
|
||||||
|
configuration: .charger(),
|
||||||
|
onPrimaryAction: onAdd
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chargerRow(for charger: SavedCharger) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
LoadIconView(
|
||||||
|
remoteIconURLString: charger.remoteIconURLString,
|
||||||
|
fallbackSystemName: charger.iconName,
|
||||||
|
fallbackColor: Color.componentColor(named: charger.colorName),
|
||||||
|
size: 48
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(charger.name)
|
||||||
|
.font(.body.weight(.medium))
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
|
||||||
|
Text(chargerSummary(for: charger))
|
||||||
|
.font(.caption.weight(.medium))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(
|
||||||
|
Capsule(style: .continuous)
|
||||||
|
.fill(Color(.tertiarySystemBackground))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if editMode == .inactive {
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsSection(for: charger)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||||
|
.fill(Color(.systemBackground))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func metricsSection(for charger: SavedCharger) -> some View {
|
||||||
|
let badges: [(String, String, Color)] = [
|
||||||
|
(badgeInputLabel, formattedVoltage(charger.inputVoltage), .indigo),
|
||||||
|
(badgeOutputLabel, formattedVoltage(charger.outputVoltage), .green),
|
||||||
|
(badgeCurrentLabel, formattedCurrent(charger.maxCurrentAmps), .orange),
|
||||||
|
(badgePowerLabel, formattedPower(charger.effectivePowerWatts), .pink)
|
||||||
|
]
|
||||||
|
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ForEach(badges, id: \.0) { label, value, tint in
|
||||||
|
ComponentMetricBadgeView(label: label, value: value, tint: tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
}
|
||||||
|
.scrollClipDisabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chargerSummary(for charger: SavedCharger) -> String {
|
||||||
|
let inputText = formattedVoltage(charger.inputVoltage)
|
||||||
|
let outputText = formattedVoltage(charger.outputVoltage)
|
||||||
|
let currentText = formattedCurrent(charger.maxCurrentAmps)
|
||||||
|
return [inputText, outputText, currentText].joined(separator: " • ")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var totalCurrent: Double {
|
||||||
|
chargers.reduce(0) { result, charger in
|
||||||
|
result + max(0, charger.maxCurrentAmps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var totalPower: Double {
|
||||||
|
chargers.reduce(0) { result, charger in
|
||||||
|
result + max(0, charger.effectivePowerWatts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var representativeOutputVoltage: Double? {
|
||||||
|
let outputs = chargers.map { $0.outputVoltage }.filter { $0 > 0 }
|
||||||
|
guard !outputs.isEmpty else { return nil }
|
||||||
|
let total = outputs.reduce(0, +)
|
||||||
|
return total / Double(outputs.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedVoltage(_ value: Double) -> String {
|
||||||
|
guard value > 0 else { return "—" }
|
||||||
|
return String(format: "%.1fV", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedCurrent(_ value: Double) -> String {
|
||||||
|
guard value > 0 else { return "—" }
|
||||||
|
return String(format: "%.1fA", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formattedPower(_ value: Double) -> String {
|
||||||
|
guard value > 0 else { return "—" }
|
||||||
|
return String(format: "%.0fW", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ChargersViewPreviewData {
|
||||||
|
static let system = ElectricalSystem(name: "Preview System", iconName: "bolt", colorName: "teal")
|
||||||
|
static let chargers: [SavedCharger] = {
|
||||||
|
let shore = SavedCharger(
|
||||||
|
name: "Shore Power",
|
||||||
|
inputVoltage: 230,
|
||||||
|
outputVoltage: 14.4,
|
||||||
|
maxCurrentAmps: 40,
|
||||||
|
maxPowerWatts: 580,
|
||||||
|
iconName: "powerplug",
|
||||||
|
colorName: "orange",
|
||||||
|
system: system
|
||||||
|
)
|
||||||
|
shore.timestamp = Date(timeIntervalSinceReferenceDate: 2000)
|
||||||
|
|
||||||
|
let dcDc = SavedCharger(
|
||||||
|
name: "DC-DC Charger",
|
||||||
|
inputVoltage: 12.8,
|
||||||
|
outputVoltage: 14.2,
|
||||||
|
maxCurrentAmps: 30,
|
||||||
|
maxPowerWatts: 0,
|
||||||
|
iconName: "bolt.badge.clock",
|
||||||
|
colorName: "blue",
|
||||||
|
system: system
|
||||||
|
)
|
||||||
|
dcDc.timestamp = Date(timeIntervalSinceReferenceDate: 2100)
|
||||||
|
|
||||||
|
return [shore, dcDc]
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ChargersView(system: ElectricalSystem(name: "Preview System"))
|
ChargersView(
|
||||||
|
system: ChargersViewPreviewData.system,
|
||||||
|
chargers: ChargersViewPreviewData.chargers,
|
||||||
|
editMode: .constant(.inactive)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
59
Cable/Chargers/SavedCharger.swift
Normal file
59
Cable/Chargers/SavedCharger.swift
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
|
@Model
|
||||||
|
final class SavedCharger {
|
||||||
|
@Attribute(.unique) var id: UUID
|
||||||
|
var name: String
|
||||||
|
var inputVoltage: Double
|
||||||
|
var outputVoltage: Double
|
||||||
|
var maxCurrentAmps: Double
|
||||||
|
var maxPowerWatts: Double
|
||||||
|
var iconName: String
|
||||||
|
var colorName: String
|
||||||
|
var system: ElectricalSystem?
|
||||||
|
var timestamp: Date
|
||||||
|
var remoteIconURLString: String?
|
||||||
|
var affiliateURLString: String?
|
||||||
|
var affiliateCountryCode: String?
|
||||||
|
var identifier: String
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: UUID = UUID(),
|
||||||
|
name: String,
|
||||||
|
inputVoltage: Double = 230.0,
|
||||||
|
outputVoltage: Double = 14.2,
|
||||||
|
maxCurrentAmps: Double = 30.0,
|
||||||
|
maxPowerWatts: Double = 0.0,
|
||||||
|
iconName: String = "bolt.fill",
|
||||||
|
colorName: String = "orange",
|
||||||
|
system: ElectricalSystem? = nil,
|
||||||
|
timestamp: Date = Date(),
|
||||||
|
remoteIconURLString: String? = nil,
|
||||||
|
affiliateURLString: String? = nil,
|
||||||
|
affiliateCountryCode: String? = nil,
|
||||||
|
identifier: String = UUID().uuidString
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.inputVoltage = inputVoltage
|
||||||
|
self.outputVoltage = outputVoltage
|
||||||
|
self.maxCurrentAmps = maxCurrentAmps
|
||||||
|
self.maxPowerWatts = maxPowerWatts
|
||||||
|
self.iconName = iconName
|
||||||
|
self.colorName = colorName
|
||||||
|
self.system = system
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.remoteIconURLString = remoteIconURLString
|
||||||
|
self.affiliateURLString = affiliateURLString
|
||||||
|
self.affiliateCountryCode = affiliateCountryCode
|
||||||
|
self.identifier = identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectivePowerWatts: Double {
|
||||||
|
if maxPowerWatts > 0 {
|
||||||
|
return maxPowerWatts
|
||||||
|
}
|
||||||
|
return outputVoltage * maxCurrentAmps
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,3 +75,77 @@ struct LoadIconView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ComponentSummaryMetricView: View {
|
||||||
|
let icon: String
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
let tint: Color
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.font(.system(size: 14, weight: .semibold))
|
||||||
|
.foregroundStyle(tint)
|
||||||
|
Text(value)
|
||||||
|
.font(.body.weight(.semibold))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.85)
|
||||||
|
}
|
||||||
|
Text(label.uppercased())
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ComponentMetricBadgeView: View {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
let tint: Color
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(label.uppercased())
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(value)
|
||||||
|
.font(.subheadline.weight(.semibold))
|
||||||
|
.foregroundStyle(tint)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.85)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||||
|
.fill(tint.opacity(0.12))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
static func componentColor(named colorName: String) -> Color {
|
||||||
|
switch colorName {
|
||||||
|
case "blue": return .blue
|
||||||
|
case "green": return .green
|
||||||
|
case "orange": return .orange
|
||||||
|
case "red": return .red
|
||||||
|
case "purple": return .purple
|
||||||
|
case "yellow": return .yellow
|
||||||
|
case "pink": return .pink
|
||||||
|
case "teal": return .teal
|
||||||
|
case "indigo": return .indigo
|
||||||
|
case "mint": return .mint
|
||||||
|
case "cyan": return .cyan
|
||||||
|
case "brown": return .brown
|
||||||
|
case "gray": return .gray
|
||||||
|
default: return .blue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct LoadsView: View {
|
|||||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||||
@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
|
@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
|
||||||
@Query(sort: \SavedBattery.timestamp, order: .reverse) private var allBatteries: [SavedBattery]
|
@Query(sort: \SavedBattery.timestamp, order: .reverse) private var allBatteries: [SavedBattery]
|
||||||
|
@Query(sort: \SavedCharger.timestamp, order: .reverse) private var allChargers: [SavedCharger]
|
||||||
@State private var newLoadToEdit: SavedLoad?
|
@State private var newLoadToEdit: SavedLoad?
|
||||||
@State private var showingSystemEditor = false
|
@State private var showingSystemEditor = false
|
||||||
@State private var hasPresentedSystemEditorOnAppear = false
|
@State private var hasPresentedSystemEditorOnAppear = false
|
||||||
@@ -22,6 +23,7 @@ struct LoadsView: View {
|
|||||||
@State private var showingSystemBOM = false
|
@State private var showingSystemBOM = false
|
||||||
@State private var selectedComponentTab: ComponentTab = .overview
|
@State private var selectedComponentTab: ComponentTab = .overview
|
||||||
@State private var batteryDraft: BatteryConfiguration?
|
@State private var batteryDraft: BatteryConfiguration?
|
||||||
|
@State private var chargerDraft: ChargerConfiguration?
|
||||||
@State private var activeStatus: LoadConfigurationStatus?
|
@State private var activeStatus: LoadConfigurationStatus?
|
||||||
@State private var editMode: EditMode = .inactive
|
@State private var editMode: EditMode = .inactive
|
||||||
|
|
||||||
@@ -43,6 +45,10 @@ struct LoadsView: View {
|
|||||||
allBatteries.filter { $0.system == system }
|
allBatteries.filter { $0.system == system }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var savedChargers: [SavedCharger] {
|
||||||
|
allChargers.filter { $0.system == system }
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
TabView(selection: $selectedComponentTab) {
|
TabView(selection: $selectedComponentTab) {
|
||||||
@@ -58,6 +64,7 @@ struct LoadsView: View {
|
|||||||
systemImage: "rectangle.3.group"
|
systemImage: "rectangle.3.group"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentsTab
|
componentsTab
|
||||||
.tag(ComponentTab.components)
|
.tag(ComponentTab.components)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
@@ -70,47 +77,57 @@ struct LoadsView: View {
|
|||||||
systemImage: "square.stack.3d.up"
|
systemImage: "square.stack.3d.up"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Group {
|
|
||||||
if savedBatteries.isEmpty {
|
Group {
|
||||||
OnboardingInfoView(
|
if savedBatteries.isEmpty {
|
||||||
configuration: .battery(),
|
OnboardingInfoView(
|
||||||
onPrimaryAction: { startBatteryConfiguration() }
|
configuration: .battery(),
|
||||||
)
|
onPrimaryAction: { startBatteryConfiguration() }
|
||||||
} else {
|
|
||||||
BatteriesView(
|
|
||||||
system: system,
|
|
||||||
batteries: savedBatteries,
|
|
||||||
editMode: $editMode,
|
|
||||||
onEdit: { editBattery($0) },
|
|
||||||
onDelete: deleteBatteries
|
|
||||||
)
|
|
||||||
.environment(\.editMode, $editMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tag(ComponentTab.batteries)
|
|
||||||
.tabItem {
|
|
||||||
Label(
|
|
||||||
String(
|
|
||||||
localized: "tab.batteries",
|
|
||||||
bundle: .main,
|
|
||||||
comment: "Tab title for battery configurations"
|
|
||||||
),
|
|
||||||
systemImage: "battery.100"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.environment(\.editMode, $editMode)
|
|
||||||
ChargersView(system: system)
|
|
||||||
.tag(ComponentTab.chargers)
|
|
||||||
.tabItem {
|
|
||||||
Label(
|
|
||||||
String(
|
|
||||||
localized: "tab.chargers",
|
|
||||||
bundle: .main,
|
|
||||||
comment: "Tab title for chargers view"
|
|
||||||
),
|
|
||||||
systemImage: "bolt.fill"
|
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
BatteriesView(
|
||||||
|
system: system,
|
||||||
|
batteries: savedBatteries,
|
||||||
|
editMode: $editMode,
|
||||||
|
onEdit: { editBattery($0) },
|
||||||
|
onDelete: deleteBatteries
|
||||||
|
)
|
||||||
|
.environment(\.editMode, $editMode)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.tag(ComponentTab.batteries)
|
||||||
|
.tabItem {
|
||||||
|
Label(
|
||||||
|
String(
|
||||||
|
localized: "tab.batteries",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Tab title for battery configurations"
|
||||||
|
),
|
||||||
|
systemImage: "battery.100"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.environment(\.editMode, $editMode)
|
||||||
|
|
||||||
|
ChargersView(
|
||||||
|
system: system,
|
||||||
|
chargers: savedChargers,
|
||||||
|
editMode: $editMode,
|
||||||
|
onAdd: { startChargerConfiguration() },
|
||||||
|
onEdit: { editCharger($0) },
|
||||||
|
onDelete: deleteChargers
|
||||||
|
)
|
||||||
|
.tag(ComponentTab.chargers)
|
||||||
|
.tabItem {
|
||||||
|
Label(
|
||||||
|
String(
|
||||||
|
localized: "tab.chargers",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Tab title for chargers view"
|
||||||
|
),
|
||||||
|
systemImage: "bolt.fill"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.environment(\.editMode, $editMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@@ -122,7 +139,7 @@ struct LoadsView: View {
|
|||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 6)
|
RoundedRectangle(cornerRadius: 6)
|
||||||
.fill(colorForName(system.colorName))
|
.fill(Color.componentColor(named: system.colorName))
|
||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
|
|
||||||
Image(systemName: system.iconName)
|
Image(systemName: system.iconName)
|
||||||
@@ -142,8 +159,9 @@ struct LoadsView: View {
|
|||||||
let showPrimary = selectedComponentTab != .overview
|
let showPrimary = selectedComponentTab != .overview
|
||||||
let showEditLoads = selectedComponentTab == .components && !savedLoads.isEmpty
|
let showEditLoads = selectedComponentTab == .components && !savedLoads.isEmpty
|
||||||
let showEditBatteries = selectedComponentTab == .batteries && !savedBatteries.isEmpty
|
let showEditBatteries = selectedComponentTab == .batteries && !savedBatteries.isEmpty
|
||||||
|
let showEditChargers = selectedComponentTab == .chargers && !savedChargers.isEmpty
|
||||||
|
|
||||||
if showPrimary || showEditLoads || showEditBatteries {
|
if showPrimary || showEditLoads || showEditBatteries || showEditChargers {
|
||||||
HStack {
|
HStack {
|
||||||
if showPrimary {
|
if showPrimary {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -151,7 +169,6 @@ struct LoadsView: View {
|
|||||||
}) {
|
}) {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
.disabled(selectedComponentTab == .chargers)
|
|
||||||
}
|
}
|
||||||
if showEditLoads {
|
if showEditLoads {
|
||||||
EditButton()
|
EditButton()
|
||||||
@@ -159,6 +176,9 @@ struct LoadsView: View {
|
|||||||
} else if showEditBatteries {
|
} else if showEditBatteries {
|
||||||
EditButton()
|
EditButton()
|
||||||
.disabled(savedBatteries.isEmpty)
|
.disabled(savedBatteries.isEmpty)
|
||||||
|
} else if showEditChargers {
|
||||||
|
EditButton()
|
||||||
|
.disabled(savedChargers.isEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,6 +196,15 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.navigationDestination(item: $chargerDraft) { draft in
|
||||||
|
ChargerEditorView(
|
||||||
|
configuration: draft,
|
||||||
|
onSave: { configuration in
|
||||||
|
saveCharger(configuration)
|
||||||
|
chargerDraft = nil
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.sheet(isPresented: $showingComponentLibrary) {
|
.sheet(isPresented: $showingComponentLibrary) {
|
||||||
ComponentLibraryView { item in
|
ComponentLibraryView { item in
|
||||||
addComponent(item)
|
addComponent(item)
|
||||||
@@ -263,35 +292,12 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var summarySection: some View {
|
private var summarySection: some View {
|
||||||
VStack(spacing: 0) {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .firstTextBaseline) {
|
||||||
Text(loadsSummaryTitle)
|
Text(loadsSummaryTitle)
|
||||||
.font(.headline.weight(.semibold))
|
.font(.headline.weight(.semibold))
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
|
||||||
showingComponentLibrary = true
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 6) {
|
|
||||||
Image(systemName: "books.vertical")
|
|
||||||
Text(
|
|
||||||
String(
|
|
||||||
localized: "loads.library.button",
|
|
||||||
bundle: .main,
|
|
||||||
comment: "Button title to open component library"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.font(.footnote.weight(.semibold))
|
|
||||||
.padding(.horizontal, 10)
|
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(
|
|
||||||
Capsule(style: .continuous)
|
|
||||||
.fill(Color(.tertiarySystemFill))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.foregroundStyle(Color.accentColor)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewThatFits(in: .horizontal) {
|
ViewThatFits(in: .horizontal) {
|
||||||
@@ -353,9 +359,35 @@ struct LoadsView: View {
|
|||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.background(Color(.separator))
|
.background(Color(.separator))
|
||||||
|
|
||||||
|
libraryButton
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
.padding(.bottom, 6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var libraryButton: some View {
|
||||||
|
Button {
|
||||||
|
showingComponentLibrary = true
|
||||||
|
} label: {
|
||||||
|
Label(
|
||||||
|
String(
|
||||||
|
localized: "loads.library.button",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Button title to open component library"
|
||||||
|
),
|
||||||
|
systemImage: "books.vertical"
|
||||||
|
)
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.padding(.horizontal, 14)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.background(.ultraThinMaterial, in: Capsule(style: .continuous))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.tint(.accentColor)
|
||||||
|
.shadow(color: Color.black.opacity(0.08), radius: 16, x: 0, y: 8)
|
||||||
|
}
|
||||||
|
|
||||||
private var componentsTab: some View {
|
private var componentsTab: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if savedLoads.isEmpty {
|
if savedLoads.isEmpty {
|
||||||
@@ -402,7 +434,7 @@ struct LoadsView: View {
|
|||||||
LoadIconView(
|
LoadIconView(
|
||||||
remoteIconURLString: load.remoteIconURLString,
|
remoteIconURLString: load.remoteIconURLString,
|
||||||
fallbackSystemName: load.iconName,
|
fallbackSystemName: load.iconName,
|
||||||
fallbackColor: colorForName(load.colorName),
|
fallbackColor: Color.componentColor(named: load.colorName),
|
||||||
size: 48
|
size: 48
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -611,19 +643,12 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
|
private func summaryMetric(icon: String, label: String, value: String, tint: Color) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
ComponentSummaryMetricView(
|
||||||
HStack(spacing: 6) {
|
icon: icon,
|
||||||
Image(systemName: icon)
|
label: label,
|
||||||
.font(.system(size: 14, weight: .semibold))
|
value: value,
|
||||||
.foregroundStyle(tint)
|
tint: tint
|
||||||
Text(value)
|
)
|
||||||
.font(.body.weight(.semibold))
|
|
||||||
}
|
|
||||||
Text(label.uppercased())
|
|
||||||
.font(.caption2)
|
|
||||||
.fontWeight(.medium)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func statusBanner(for status: LoadConfigurationStatus) -> some View {
|
private func statusBanner(for status: LoadConfigurationStatus) -> some View {
|
||||||
@@ -663,20 +688,10 @@ struct LoadsView: View {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
private func metricBadge(label: String, value: String, tint: Color) -> some View {
|
private func metricBadge(label: String, value: String, tint: Color) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
ComponentMetricBadgeView(
|
||||||
Text(label.uppercased())
|
label: label,
|
||||||
.font(.caption2)
|
value: value,
|
||||||
.fontWeight(.medium)
|
tint: tint
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Text(value)
|
|
||||||
.font(.subheadline.weight(.semibold))
|
|
||||||
.foregroundStyle(tint)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
||||||
.fill(tint.opacity(0.12))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,7 +712,7 @@ struct LoadsView: View {
|
|||||||
case .batteries:
|
case .batteries:
|
||||||
startBatteryConfiguration()
|
startBatteryConfiguration()
|
||||||
case .chargers:
|
case .chargers:
|
||||||
break
|
startChargerConfiguration()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,7 +721,8 @@ struct LoadsView: View {
|
|||||||
for: system,
|
for: system,
|
||||||
in: modelContext,
|
in: modelContext,
|
||||||
existingLoads: savedLoads,
|
existingLoads: savedLoads,
|
||||||
existingBatteries: savedBatteries
|
existingBatteries: savedBatteries,
|
||||||
|
existingChargers: savedChargers
|
||||||
)
|
)
|
||||||
newLoadToEdit = newLoad
|
newLoadToEdit = newLoad
|
||||||
}
|
}
|
||||||
@@ -715,7 +731,8 @@ struct LoadsView: View {
|
|||||||
batteryDraft = SystemComponentsPersistence.makeBatteryDraft(
|
batteryDraft = SystemComponentsPersistence.makeBatteryDraft(
|
||||||
for: system,
|
for: system,
|
||||||
existingLoads: savedLoads,
|
existingLoads: savedLoads,
|
||||||
existingBatteries: savedBatteries
|
existingBatteries: savedBatteries,
|
||||||
|
existingChargers: savedChargers
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -742,36 +759,50 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func startChargerConfiguration() {
|
||||||
|
chargerDraft = SystemComponentsPersistence.makeChargerDraft(
|
||||||
|
for: system,
|
||||||
|
existingLoads: savedLoads,
|
||||||
|
existingBatteries: savedBatteries,
|
||||||
|
existingChargers: savedChargers
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveCharger(_ configuration: ChargerConfiguration) {
|
||||||
|
SystemComponentsPersistence.saveCharger(
|
||||||
|
configuration,
|
||||||
|
for: system,
|
||||||
|
existingChargers: savedChargers,
|
||||||
|
in: modelContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func editCharger(_ charger: SavedCharger) {
|
||||||
|
chargerDraft = ChargerConfiguration(savedCharger: charger, system: system)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteChargers(_ offsets: IndexSet) {
|
||||||
|
withAnimation {
|
||||||
|
SystemComponentsPersistence.deleteChargers(
|
||||||
|
at: offsets,
|
||||||
|
from: savedChargers,
|
||||||
|
in: modelContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func addComponent(_ item: ComponentLibraryItem) {
|
private func addComponent(_ item: ComponentLibraryItem) {
|
||||||
let newLoad = SystemComponentsPersistence.createLoad(
|
let newLoad = SystemComponentsPersistence.createLoad(
|
||||||
from: item,
|
from: item,
|
||||||
for: system,
|
for: system,
|
||||||
in: modelContext,
|
in: modelContext,
|
||||||
existingLoads: savedLoads,
|
existingLoads: savedLoads,
|
||||||
existingBatteries: savedBatteries
|
existingBatteries: savedBatteries,
|
||||||
|
existingChargers: savedChargers
|
||||||
)
|
)
|
||||||
newLoadToEdit = newLoad
|
newLoadToEdit = newLoad
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorForName(_ colorName: String) -> Color {
|
|
||||||
switch colorName {
|
|
||||||
case "blue": return .blue
|
|
||||||
case "green": return .green
|
|
||||||
case "orange": return .orange
|
|
||||||
case "red": return .red
|
|
||||||
case "purple": return .purple
|
|
||||||
case "yellow": return .yellow
|
|
||||||
case "pink": return .pink
|
|
||||||
case "teal": return .teal
|
|
||||||
case "indigo": return .indigo
|
|
||||||
case "mint": return .mint
|
|
||||||
case "cyan": return .cyan
|
|
||||||
case "brown": return .brown
|
|
||||||
case "gray": return .gray
|
|
||||||
default: return .blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func awgFromCrossSection(_ crossSectionMM2: Double) -> Double {
|
private func awgFromCrossSection(_ crossSectionMM2: Double) -> Double {
|
||||||
let awgSizes = [(20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31),
|
let awgSizes = [(20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31),
|
||||||
(10, 5.26), (8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6),
|
(10, 5.26), (8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6),
|
||||||
|
|||||||
@@ -140,9 +140,21 @@ extension OnboardingInfoView.Configuration {
|
|||||||
secondaryActionTitle: nil,
|
secondaryActionTitle: nil,
|
||||||
secondaryActionIcon: nil,
|
secondaryActionIcon: nil,
|
||||||
imageNames: [
|
imageNames: [
|
||||||
"charger-onboarding",
|
"battery-onboarding"
|
||||||
"router-onboarding",
|
]
|
||||||
"coffee-onboarding"
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func charger() -> Self {
|
||||||
|
Self(
|
||||||
|
title: LocalizedStringKey("chargers.onboarding.title"),
|
||||||
|
subtitle: LocalizedStringKey("chargers.onboarding.subtitle"),
|
||||||
|
primaryActionTitle: LocalizedStringKey("chargers.onboarding.primary"),
|
||||||
|
primaryActionIcon: "plus",
|
||||||
|
secondaryActionTitle: nil,
|
||||||
|
secondaryActionIcon: nil,
|
||||||
|
imageNames: [
|
||||||
|
"charger-onboarding"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ struct SystemOverviewView: View {
|
|||||||
HStack(spacing: 14) {
|
HStack(spacing: 14) {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.fill(colorForName(system.colorName))
|
.fill(Color.componentColor(named: system.colorName))
|
||||||
.frame(width: 54, height: 54)
|
.frame(width: 54, height: 54)
|
||||||
Image(systemName: system.iconName)
|
Image(systemName: system.iconName)
|
||||||
.font(.system(size: 24, weight: .semibold))
|
.font(.system(size: 24, weight: .semibold))
|
||||||
@@ -513,25 +513,6 @@ struct SystemOverviewView: View {
|
|||||||
!batteries.isEmpty && !loads.isEmpty && formattedRuntime == nil
|
!batteries.isEmpty && !loads.isEmpty && formattedRuntime == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorForName(_ colorName: String) -> Color {
|
|
||||||
switch colorName {
|
|
||||||
case "blue": return .blue
|
|
||||||
case "green": return .green
|
|
||||||
case "orange": return .orange
|
|
||||||
case "red": return .red
|
|
||||||
case "purple": return .purple
|
|
||||||
case "yellow": return .yellow
|
|
||||||
case "pink": return .pink
|
|
||||||
case "teal": return .teal
|
|
||||||
case "indigo": return .indigo
|
|
||||||
case "mint": return .mint
|
|
||||||
case "cyan": return .cyan
|
|
||||||
case "brown": return .brown
|
|
||||||
case "gray": return .gray
|
|
||||||
default: return .blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var loadsSummaryTitle: String {
|
private var loadsSummaryTitle: String {
|
||||||
NSLocalizedString(
|
NSLocalizedString(
|
||||||
"loads.overview.header.title",
|
"loads.overview.header.title",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ struct SystemComponentsPersistence {
|
|||||||
for system: ElectricalSystem,
|
for system: ElectricalSystem,
|
||||||
in context: ModelContext,
|
in context: ModelContext,
|
||||||
existingLoads: [SavedLoad],
|
existingLoads: [SavedLoad],
|
||||||
existingBatteries: [SavedBattery]
|
existingBatteries: [SavedBattery],
|
||||||
|
existingChargers: [SavedCharger]
|
||||||
) -> SavedLoad {
|
) -> SavedLoad {
|
||||||
let defaultName = String(
|
let defaultName = String(
|
||||||
localized: "default.load.new",
|
localized: "default.load.new",
|
||||||
@@ -16,7 +17,8 @@ struct SystemComponentsPersistence {
|
|||||||
let loadName = uniqueName(
|
let loadName = uniqueName(
|
||||||
startingWith: defaultName,
|
startingWith: defaultName,
|
||||||
loads: existingLoads,
|
loads: existingLoads,
|
||||||
batteries: existingBatteries
|
batteries: existingBatteries,
|
||||||
|
chargers: existingChargers
|
||||||
)
|
)
|
||||||
let newLoad = SavedLoad(
|
let newLoad = SavedLoad(
|
||||||
name: loadName,
|
name: loadName,
|
||||||
@@ -40,14 +42,16 @@ struct SystemComponentsPersistence {
|
|||||||
for system: ElectricalSystem,
|
for system: ElectricalSystem,
|
||||||
in context: ModelContext,
|
in context: ModelContext,
|
||||||
existingLoads: [SavedLoad],
|
existingLoads: [SavedLoad],
|
||||||
existingBatteries: [SavedBattery]
|
existingBatteries: [SavedBattery],
|
||||||
|
existingChargers: [SavedCharger]
|
||||||
) -> SavedLoad {
|
) -> SavedLoad {
|
||||||
let localizedName = item.localizedName
|
let localizedName = item.localizedName
|
||||||
let baseName = localizedName.isEmpty ? "Library Load" : localizedName
|
let baseName = localizedName.isEmpty ? "Library Load" : localizedName
|
||||||
let loadName = uniqueName(
|
let loadName = uniqueName(
|
||||||
startingWith: baseName,
|
startingWith: baseName,
|
||||||
loads: existingLoads,
|
loads: existingLoads,
|
||||||
batteries: existingBatteries
|
batteries: existingBatteries,
|
||||||
|
chargers: existingChargers
|
||||||
)
|
)
|
||||||
let voltage = item.displayVoltage ?? 12.0
|
let voltage = item.displayVoltage ?? 12.0
|
||||||
let power = item.watt ?? (item.current != nil ? item.current! * voltage : 0)
|
let power = item.watt ?? (item.current != nil ? item.current! * voltage : 0)
|
||||||
@@ -85,7 +89,8 @@ struct SystemComponentsPersistence {
|
|||||||
static func makeBatteryDraft(
|
static func makeBatteryDraft(
|
||||||
for system: ElectricalSystem,
|
for system: ElectricalSystem,
|
||||||
existingLoads: [SavedLoad],
|
existingLoads: [SavedLoad],
|
||||||
existingBatteries: [SavedBattery]
|
existingBatteries: [SavedBattery],
|
||||||
|
existingChargers: [SavedCharger]
|
||||||
) -> BatteryConfiguration {
|
) -> BatteryConfiguration {
|
||||||
let defaultName = NSLocalizedString(
|
let defaultName = NSLocalizedString(
|
||||||
"battery.editor.default_name",
|
"battery.editor.default_name",
|
||||||
@@ -96,7 +101,8 @@ struct SystemComponentsPersistence {
|
|||||||
let batteryName = uniqueName(
|
let batteryName = uniqueName(
|
||||||
startingWith: defaultName,
|
startingWith: defaultName,
|
||||||
loads: existingLoads,
|
loads: existingLoads,
|
||||||
batteries: existingBatteries
|
batteries: existingBatteries,
|
||||||
|
chargers: existingChargers
|
||||||
)
|
)
|
||||||
return BatteryConfiguration(
|
return BatteryConfiguration(
|
||||||
name: batteryName,
|
name: batteryName,
|
||||||
@@ -106,6 +112,32 @@ struct SystemComponentsPersistence {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func makeChargerDraft(
|
||||||
|
for system: ElectricalSystem,
|
||||||
|
existingLoads: [SavedLoad],
|
||||||
|
existingBatteries: [SavedBattery],
|
||||||
|
existingChargers: [SavedCharger]
|
||||||
|
) -> ChargerConfiguration {
|
||||||
|
let defaultName = NSLocalizedString(
|
||||||
|
"charger.editor.default_name",
|
||||||
|
bundle: .main,
|
||||||
|
value: "New Charger",
|
||||||
|
comment: "Default name when configuring a new charger"
|
||||||
|
)
|
||||||
|
let chargerName = uniqueName(
|
||||||
|
startingWith: defaultName,
|
||||||
|
loads: existingLoads,
|
||||||
|
batteries: existingBatteries,
|
||||||
|
chargers: existingChargers
|
||||||
|
)
|
||||||
|
return ChargerConfiguration(
|
||||||
|
name: chargerName,
|
||||||
|
iconName: "bolt.fill",
|
||||||
|
colorName: system.colorName,
|
||||||
|
system: system
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
static func saveBattery(
|
static func saveBattery(
|
||||||
_ configuration: BatteryConfiguration,
|
_ configuration: BatteryConfiguration,
|
||||||
for system: ElectricalSystem,
|
for system: ElectricalSystem,
|
||||||
@@ -129,6 +161,30 @@ struct SystemComponentsPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func saveCharger(
|
||||||
|
_ configuration: ChargerConfiguration,
|
||||||
|
for system: ElectricalSystem,
|
||||||
|
existingChargers: [SavedCharger],
|
||||||
|
in context: ModelContext
|
||||||
|
) {
|
||||||
|
if let existing = existingChargers.first(where: { $0.id == configuration.id }) {
|
||||||
|
configuration.apply(to: existing)
|
||||||
|
} else {
|
||||||
|
let newCharger = SavedCharger(
|
||||||
|
id: configuration.id,
|
||||||
|
name: configuration.name,
|
||||||
|
inputVoltage: configuration.inputVoltage,
|
||||||
|
outputVoltage: configuration.outputVoltage,
|
||||||
|
maxCurrentAmps: configuration.maxCurrentAmps,
|
||||||
|
maxPowerWatts: configuration.maxPowerWatts,
|
||||||
|
iconName: configuration.iconName,
|
||||||
|
colorName: configuration.colorName,
|
||||||
|
system: system
|
||||||
|
)
|
||||||
|
context.insert(newCharger)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func deleteBatteries(
|
static func deleteBatteries(
|
||||||
at offsets: IndexSet,
|
at offsets: IndexSet,
|
||||||
from batteries: [SavedBattery],
|
from batteries: [SavedBattery],
|
||||||
@@ -139,12 +195,27 @@ struct SystemComponentsPersistence {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func deleteChargers(
|
||||||
|
at offsets: IndexSet,
|
||||||
|
from chargers: [SavedCharger],
|
||||||
|
in context: ModelContext
|
||||||
|
) {
|
||||||
|
for index in offsets {
|
||||||
|
context.delete(chargers[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func uniqueName(
|
static func uniqueName(
|
||||||
startingWith baseName: String,
|
startingWith baseName: String,
|
||||||
loads: [SavedLoad],
|
loads: [SavedLoad],
|
||||||
batteries: [SavedBattery]
|
batteries: [SavedBattery],
|
||||||
|
chargers: [SavedCharger]
|
||||||
) -> String {
|
) -> String {
|
||||||
let existingNames = Set(loads.map { $0.name } + batteries.map { $0.name })
|
let existingNames = Set(
|
||||||
|
loads.map { $0.name } +
|
||||||
|
batteries.map { $0.name } +
|
||||||
|
chargers.map { $0.name }
|
||||||
|
)
|
||||||
|
|
||||||
if !existingNames.contains(baseName) {
|
if !existingNames.contains(baseName) {
|
||||||
return baseName
|
return baseName
|
||||||
@@ -160,4 +231,35 @@ struct SystemComponentsPersistence {
|
|||||||
|
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func createDefaultCharger(
|
||||||
|
for system: ElectricalSystem,
|
||||||
|
in context: ModelContext,
|
||||||
|
existingLoads: [SavedLoad],
|
||||||
|
existingBatteries: [SavedBattery],
|
||||||
|
existingChargers: [SavedCharger]
|
||||||
|
) -> SavedCharger {
|
||||||
|
let defaultName = String(
|
||||||
|
localized: "charger.default.new",
|
||||||
|
bundle: .main,
|
||||||
|
comment: "Default name when creating a new charger from system view"
|
||||||
|
)
|
||||||
|
let chargerName = uniqueName(
|
||||||
|
startingWith: defaultName,
|
||||||
|
loads: existingLoads,
|
||||||
|
batteries: existingBatteries,
|
||||||
|
chargers: existingChargers
|
||||||
|
)
|
||||||
|
let charger = SavedCharger(
|
||||||
|
name: chargerName,
|
||||||
|
inputVoltage: 230,
|
||||||
|
outputVoltage: 14.4,
|
||||||
|
maxCurrentAmps: 30,
|
||||||
|
iconName: "bolt.fill",
|
||||||
|
colorName: system.colorName,
|
||||||
|
system: system
|
||||||
|
)
|
||||||
|
context.insert(charger)
|
||||||
|
return charger
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ struct SystemsView: View {
|
|||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
ZStack {
|
ZStack {
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: 10)
|
||||||
.fill(colorForName(system.colorName))
|
.fill(Color.componentColor(named: system.colorName))
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 44, height: 44)
|
||||||
|
|
||||||
Image(systemName: system.iconName)
|
Image(systemName: system.iconName)
|
||||||
@@ -395,24 +395,6 @@ struct SystemsView: View {
|
|||||||
return uniqueKeywords
|
return uniqueKeywords
|
||||||
}
|
}
|
||||||
|
|
||||||
private func colorForName(_ colorName: String) -> Color {
|
|
||||||
switch colorName {
|
|
||||||
case "blue": return .blue
|
|
||||||
case "green": return .green
|
|
||||||
case "orange": return .orange
|
|
||||||
case "red": return .red
|
|
||||||
case "purple": return .purple
|
|
||||||
case "yellow": return .yellow
|
|
||||||
case "pink": return .pink
|
|
||||||
case "teal": return .teal
|
|
||||||
case "indigo": return .indigo
|
|
||||||
case "mint": return .mint
|
|
||||||
case "cyan": return .cyan
|
|
||||||
case "brown": return .brown
|
|
||||||
case "gray": return .gray
|
|
||||||
default: return .blue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview("Sample Systems") {
|
#Preview("Sample Systems") {
|
||||||
|
|||||||
@@ -31,14 +31,20 @@ private extension UITestSampleData {
|
|||||||
static func clearExistingData(in context: ModelContext) throws {
|
static func clearExistingData(in context: ModelContext) throws {
|
||||||
let systemDescriptor = FetchDescriptor<ElectricalSystem>()
|
let systemDescriptor = FetchDescriptor<ElectricalSystem>()
|
||||||
let loadDescriptor = FetchDescriptor<SavedLoad>()
|
let loadDescriptor = FetchDescriptor<SavedLoad>()
|
||||||
|
let batteryDescriptor = FetchDescriptor<SavedBattery>()
|
||||||
|
let chargerDescriptor = FetchDescriptor<SavedCharger>()
|
||||||
let itemDescriptor = FetchDescriptor<Item>()
|
let itemDescriptor = FetchDescriptor<Item>()
|
||||||
|
|
||||||
let systems = try context.fetch(systemDescriptor)
|
let systems = try context.fetch(systemDescriptor)
|
||||||
let loads = try context.fetch(loadDescriptor)
|
let loads = try context.fetch(loadDescriptor)
|
||||||
|
let batteries = try context.fetch(batteryDescriptor)
|
||||||
|
let chargers = try context.fetch(chargerDescriptor)
|
||||||
let items = try context.fetch(itemDescriptor)
|
let items = try context.fetch(itemDescriptor)
|
||||||
|
|
||||||
systems.forEach { context.delete($0) }
|
systems.forEach { context.delete($0) }
|
||||||
loads.forEach { context.delete($0) }
|
loads.forEach { context.delete($0) }
|
||||||
|
batteries.forEach { context.delete($0) }
|
||||||
|
chargers.forEach { context.delete($0) }
|
||||||
items.forEach { context.delete($0) }
|
items.forEach { context.delete($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +129,47 @@ private extension UITestSampleData {
|
|||||||
workshopCharger.timestamp = Date(timeIntervalSinceReferenceDate: 2200)
|
workshopCharger.timestamp = Date(timeIntervalSinceReferenceDate: 2200)
|
||||||
|
|
||||||
[vanFridge, vanLighting, workshopCompressor, workshopCharger].forEach { context.insert($0) }
|
[vanFridge, vanLighting, workshopCompressor, workshopCharger].forEach { context.insert($0) }
|
||||||
|
|
||||||
|
let shoreCharger = SavedCharger(
|
||||||
|
name: String(localized: "sample.charger.shore.name", comment: "Sample data name for a shore power charger"),
|
||||||
|
inputVoltage: 230.0,
|
||||||
|
outputVoltage: 14.4,
|
||||||
|
maxCurrentAmps: 40.0,
|
||||||
|
maxPowerWatts: 600.0,
|
||||||
|
iconName: "powerplug",
|
||||||
|
colorName: "orange",
|
||||||
|
system: adventureVan,
|
||||||
|
identifier: "sample.charger.shore"
|
||||||
|
)
|
||||||
|
shoreCharger.timestamp = Date(timeIntervalSinceReferenceDate: 1300)
|
||||||
|
|
||||||
|
let alternatorCharger = SavedCharger(
|
||||||
|
name: String(localized: "sample.charger.dcdc.name", comment: "Sample data name for a DC-DC charger"),
|
||||||
|
inputVoltage: 12.8,
|
||||||
|
outputVoltage: 14.2,
|
||||||
|
maxCurrentAmps: 30.0,
|
||||||
|
maxPowerWatts: 0.0,
|
||||||
|
iconName: "bolt.badge.clock",
|
||||||
|
colorName: "blue",
|
||||||
|
system: adventureVan,
|
||||||
|
identifier: "sample.charger.dcdc"
|
||||||
|
)
|
||||||
|
alternatorCharger.timestamp = Date(timeIntervalSinceReferenceDate: 1350)
|
||||||
|
|
||||||
|
let benchCharger = SavedCharger(
|
||||||
|
name: String(localized: "sample.charger.workbench.name", comment: "Sample data name for a workbench charger"),
|
||||||
|
inputVoltage: 120.0,
|
||||||
|
outputVoltage: 14.6,
|
||||||
|
maxCurrentAmps: 25.0,
|
||||||
|
maxPowerWatts: 365.0,
|
||||||
|
iconName: "bolt",
|
||||||
|
colorName: "green",
|
||||||
|
system: workshopBench,
|
||||||
|
identifier: "sample.charger.workbench"
|
||||||
|
)
|
||||||
|
benchCharger.timestamp = Date(timeIntervalSinceReferenceDate: 2250)
|
||||||
|
|
||||||
|
[shoreCharger, alternatorCharger, benchCharger].forEach { context.insert($0) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -209,5 +209,38 @@
|
|||||||
"battery.editor.alert.save" = "Speichern";
|
"battery.editor.alert.save" = "Speichern";
|
||||||
"battery.editor.default_name" = "Neue Batterie";
|
"battery.editor.default_name" = "Neue Batterie";
|
||||||
|
|
||||||
|
"charger.editor.title" = "Ladegerät";
|
||||||
|
"charger.editor.field.name" = "Name";
|
||||||
|
"charger.editor.placeholder.name" = "Werkstattladegerät";
|
||||||
|
"charger.editor.section.electrical" = "Elektrik";
|
||||||
|
"charger.editor.section.power" = "Ladeausgang";
|
||||||
|
"charger.editor.appearance.title" = "Ladegerät-Darstellung";
|
||||||
|
"charger.editor.appearance.subtitle" = "Passe an, wie dieses Ladegerät angezeigt wird";
|
||||||
|
"charger.editor.appearance.accessibility" = "Darstellung des Ladegeräts bearbeiten";
|
||||||
|
"charger.editor.field.input_voltage" = "Eingangsspannung";
|
||||||
|
"charger.editor.field.output_voltage" = "Ausgangsspannung";
|
||||||
|
"charger.editor.field.current" = "Ladestrom";
|
||||||
|
"charger.editor.field.power" = "Ladeleistung";
|
||||||
|
"charger.editor.field.power.footer" = "Leer lassen, wenn keine Leistungsangabe vorliegt. Wir berechnen sie aus Spannung und Strom.";
|
||||||
|
"charger.editor.default_name" = "Neues Ladegerät";
|
||||||
|
"charger.default.new" = "Neues Ladegerät";
|
||||||
|
|
||||||
|
"chargers.summary.title" = "Ladeübersicht";
|
||||||
|
"chargers.summary.metric.count" = "Ladegeräte";
|
||||||
|
"chargers.summary.metric.output" = "Spannung";
|
||||||
|
"chargers.summary.metric.current" = "Ladestrom";
|
||||||
|
"chargers.summary.metric.power" = "Ladeleistung";
|
||||||
|
"chargers.badge.input" = "Eingang";
|
||||||
|
"chargers.badge.output" = "Ausgang";
|
||||||
|
"chargers.badge.current" = "Strom";
|
||||||
|
"chargers.badge.power" = "Leistung";
|
||||||
|
"chargers.onboarding.title" = "Füge deine Ladegeräte hinzu";
|
||||||
|
"chargers.onboarding.subtitle" = "Verwalte Landstrom, Booster und Solarregler, um deine Ladeleistung im Blick zu behalten.";
|
||||||
|
"chargers.onboarding.primary" = "Ladegerät erstellen";
|
||||||
|
|
||||||
|
"sample.charger.shore.name" = "Landstrom-Ladegerät";
|
||||||
|
"sample.charger.dcdc.name" = "DC-DC-Ladegerät";
|
||||||
|
"sample.charger.workbench.name" = "Werkbank-Ladegerät";
|
||||||
|
|
||||||
"chargers.title" = "Ladegeräte für %@";
|
"chargers.title" = "Ladegeräte für %@";
|
||||||
"chargers.subtitle" = "Ladegeräte-Komponenten sind bald verfügbar.";
|
"chargers.subtitle" = "Ladegeräte-Komponenten sind bald verfügbar.";
|
||||||
|
|||||||
@@ -208,5 +208,38 @@
|
|||||||
"battery.editor.alert.save" = "Guardar";
|
"battery.editor.alert.save" = "Guardar";
|
||||||
"battery.editor.default_name" = "Nueva batería";
|
"battery.editor.default_name" = "Nueva batería";
|
||||||
|
|
||||||
|
"charger.editor.title" = "Cargador";
|
||||||
|
"charger.editor.field.name" = "Nombre";
|
||||||
|
"charger.editor.placeholder.name" = "Cargador de taller";
|
||||||
|
"charger.editor.section.electrical" = "Eléctrico";
|
||||||
|
"charger.editor.section.power" = "Salida de carga";
|
||||||
|
"charger.editor.appearance.title" = "Apariencia del cargador";
|
||||||
|
"charger.editor.appearance.subtitle" = "Personaliza cómo se muestra este cargador";
|
||||||
|
"charger.editor.appearance.accessibility" = "Editar apariencia del cargador";
|
||||||
|
"charger.editor.field.input_voltage" = "Voltaje de entrada";
|
||||||
|
"charger.editor.field.output_voltage" = "Voltaje de salida";
|
||||||
|
"charger.editor.field.current" = "Corriente de carga";
|
||||||
|
"charger.editor.field.power" = "Potencia de carga";
|
||||||
|
"charger.editor.field.power.footer" = "Déjalo en blanco si no se publica la potencia nominal. La calcularemos a partir del voltaje y la corriente.";
|
||||||
|
"charger.editor.default_name" = "Nuevo cargador";
|
||||||
|
"charger.default.new" = "Nuevo cargador";
|
||||||
|
|
||||||
|
"chargers.summary.title" = "Resumen de carga";
|
||||||
|
"chargers.summary.metric.count" = "Cargadores";
|
||||||
|
"chargers.summary.metric.output" = "Voltaje de salida";
|
||||||
|
"chargers.summary.metric.current" = "Tasa de carga";
|
||||||
|
"chargers.summary.metric.power" = "Potencia de carga";
|
||||||
|
"chargers.badge.input" = "Entrada";
|
||||||
|
"chargers.badge.output" = "Salida";
|
||||||
|
"chargers.badge.current" = "Corriente";
|
||||||
|
"chargers.badge.power" = "Potencia";
|
||||||
|
"chargers.onboarding.title" = "Añade tus cargadores";
|
||||||
|
"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";
|
||||||
|
|
||||||
|
"sample.charger.shore.name" = "Cargador de costa";
|
||||||
|
"sample.charger.dcdc.name" = "Cargador DC-DC";
|
||||||
|
"sample.charger.workbench.name" = "Cargador de banco de trabajo";
|
||||||
|
|
||||||
"chargers.title" = "Cargadores para %@";
|
"chargers.title" = "Cargadores para %@";
|
||||||
"chargers.subtitle" = "Los componentes de carga estarán disponibles pronto.";
|
"chargers.subtitle" = "Los componentes de carga estarán disponibles pronto.";
|
||||||
|
|||||||
@@ -208,5 +208,38 @@
|
|||||||
"battery.editor.alert.save" = "Enregistrer";
|
"battery.editor.alert.save" = "Enregistrer";
|
||||||
"battery.editor.default_name" = "Nouvelle batterie";
|
"battery.editor.default_name" = "Nouvelle batterie";
|
||||||
|
|
||||||
|
"charger.editor.title" = "Chargeur";
|
||||||
|
"charger.editor.field.name" = "Nom";
|
||||||
|
"charger.editor.placeholder.name" = "Chargeur d'atelier";
|
||||||
|
"charger.editor.section.electrical" = "Électrique";
|
||||||
|
"charger.editor.section.power" = "Sortie de charge";
|
||||||
|
"charger.editor.appearance.title" = "Apparence du chargeur";
|
||||||
|
"charger.editor.appearance.subtitle" = "Personnalisez l'affichage de ce chargeur";
|
||||||
|
"charger.editor.appearance.accessibility" = "Modifier l'apparence du chargeur";
|
||||||
|
"charger.editor.field.input_voltage" = "Tension d'entrée";
|
||||||
|
"charger.editor.field.output_voltage" = "Tension de sortie";
|
||||||
|
"charger.editor.field.current" = "Courant de charge";
|
||||||
|
"charger.editor.field.power" = "Puissance de charge";
|
||||||
|
"charger.editor.field.power.footer" = "Laissez vide si la puissance nominale n'est pas indiquée. Nous la calculerons à partir de la tension et du courant.";
|
||||||
|
"charger.editor.default_name" = "Nouveau chargeur";
|
||||||
|
"charger.default.new" = "Nouveau chargeur";
|
||||||
|
|
||||||
|
"chargers.summary.title" = "Aperçu de charge";
|
||||||
|
"chargers.summary.metric.count" = "Chargeurs";
|
||||||
|
"chargers.summary.metric.output" = "Tension de sortie";
|
||||||
|
"chargers.summary.metric.current" = "Courant de charge";
|
||||||
|
"chargers.summary.metric.power" = "Puissance de charge";
|
||||||
|
"chargers.badge.input" = "Entrée";
|
||||||
|
"chargers.badge.output" = "Sortie";
|
||||||
|
"chargers.badge.current" = "Courant";
|
||||||
|
"chargers.badge.power" = "Puissance";
|
||||||
|
"chargers.onboarding.title" = "Ajoutez vos chargeurs";
|
||||||
|
"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";
|
||||||
|
|
||||||
|
"sample.charger.shore.name" = "Chargeur de quai";
|
||||||
|
"sample.charger.dcdc.name" = "Chargeur DC-DC";
|
||||||
|
"sample.charger.workbench.name" = "Chargeur d'établi";
|
||||||
|
|
||||||
"chargers.title" = "Chargeurs pour %@";
|
"chargers.title" = "Chargeurs pour %@";
|
||||||
"chargers.subtitle" = "Les chargeurs seront bientôt disponibles ici.";
|
"chargers.subtitle" = "Les chargeurs seront bientôt disponibles ici.";
|
||||||
|
|||||||
@@ -208,5 +208,38 @@
|
|||||||
"battery.editor.alert.save" = "Opslaan";
|
"battery.editor.alert.save" = "Opslaan";
|
||||||
"battery.editor.default_name" = "Nieuwe batterij";
|
"battery.editor.default_name" = "Nieuwe batterij";
|
||||||
|
|
||||||
|
"charger.editor.title" = "Lader";
|
||||||
|
"charger.editor.field.name" = "Naam";
|
||||||
|
"charger.editor.placeholder.name" = "Werkplaatslader";
|
||||||
|
"charger.editor.section.electrical" = "Elektrisch";
|
||||||
|
"charger.editor.section.power" = "Laaduitgang";
|
||||||
|
"charger.editor.appearance.title" = "Uiterlijk van lader";
|
||||||
|
"charger.editor.appearance.subtitle" = "Bepaal hoe deze lader wordt weergegeven";
|
||||||
|
"charger.editor.appearance.accessibility" = "Uiterlijk van lader bewerken";
|
||||||
|
"charger.editor.field.input_voltage" = "Ingangsspanning";
|
||||||
|
"charger.editor.field.output_voltage" = "Uitgangsspanning";
|
||||||
|
"charger.editor.field.current" = "Laadstroom";
|
||||||
|
"charger.editor.field.power" = "Laadvermogen";
|
||||||
|
"charger.editor.field.power.footer" = "Laat leeg als het opgegeven vermogen ontbreekt. We berekenen het uit spanning en stroom.";
|
||||||
|
"charger.editor.default_name" = "Nieuwe lader";
|
||||||
|
"charger.default.new" = "Nieuwe lader";
|
||||||
|
|
||||||
|
"chargers.summary.title" = "Laadoverzicht";
|
||||||
|
"chargers.summary.metric.count" = "Laders";
|
||||||
|
"chargers.summary.metric.output" = "Uitgangsspanning";
|
||||||
|
"chargers.summary.metric.current" = "Laadstroom";
|
||||||
|
"chargers.summary.metric.power" = "Laadvermogen";
|
||||||
|
"chargers.badge.input" = "Ingang";
|
||||||
|
"chargers.badge.output" = "Uitgang";
|
||||||
|
"chargers.badge.current" = "Stroom";
|
||||||
|
"chargers.badge.power" = "Vermogen";
|
||||||
|
"chargers.onboarding.title" = "Voeg je laders toe";
|
||||||
|
"chargers.onboarding.subtitle" = "Houd walstroom, boosters en zonne-regelaars bij om je laadcapaciteit te kennen.";
|
||||||
|
"chargers.onboarding.primary" = "Lader aanmaken";
|
||||||
|
|
||||||
|
"sample.charger.shore.name" = "Walstroomlader";
|
||||||
|
"sample.charger.dcdc.name" = "DC-DC-lader";
|
||||||
|
"sample.charger.workbench.name" = "Werkplaatslader";
|
||||||
|
|
||||||
"chargers.title" = "Laders voor %@";
|
"chargers.title" = "Laders voor %@";
|
||||||
"chargers.subtitle" = "Ladercomponenten zijn binnenkort beschikbaar.";
|
"chargers.subtitle" = "Ladercomponenten zijn binnenkort beschikbaar.";
|
||||||
|
|||||||
Reference in New Issue
Block a user