better battery editor view

This commit is contained in:
Stefan Lange-Hegermann
2025-10-21 23:00:56 +02:00
parent 9f8d8e5149
commit d081a79b59
5 changed files with 188 additions and 60 deletions

View File

@@ -255,7 +255,7 @@ struct BatteriesView: View {
private func batteryRow(for battery: SavedBattery) -> some View { private func batteryRow(for battery: SavedBattery) -> some View {
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
HStack(spacing: 12) { HStack(spacing: 12) {
batteryIcon batteryIcon(for: battery)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline) { HStack(alignment: .firstTextBaseline) {
@@ -312,12 +312,12 @@ struct BatteriesView: View {
) )
} }
private var batteryIcon: some View { private func batteryIcon(for battery: SavedBattery) -> some View {
ZStack { ZStack {
RoundedRectangle(cornerRadius: 12, style: .continuous) RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(colorForName(system.colorName)) .fill(colorForName(battery.colorName))
.frame(width: 48, height: 48) .frame(width: 48, height: 48)
Image(systemName: "battery.100.bolt") Image(systemName: battery.iconName.isEmpty ? "battery.100.bolt" : battery.iconName)
.font(.system(size: 22, weight: .semibold)) .font(.system(size: 22, weight: .semibold))
.foregroundStyle(Color.white) .foregroundStyle(Color.white)
} }
@@ -589,6 +589,8 @@ private enum BatteriesViewPreviewData {
nominalVoltage: 12.8, nominalVoltage: 12.8,
capacityAmpHours: 200, capacityAmpHours: 200,
chemistry: .lithiumIronPhosphate, chemistry: .lithiumIronPhosphate,
iconName: "battery.100.bolt",
colorName: "green",
system: system system: system
), ),
SavedBattery( SavedBattery(
@@ -596,6 +598,8 @@ private enum BatteriesViewPreviewData {
nominalVoltage: 12.0, nominalVoltage: 12.0,
capacityAmpHours: 90, capacityAmpHours: 90,
chemistry: .agm, chemistry: .agm,
iconName: "bolt",
colorName: "orange",
system: system system: system
) )
] ]

View File

@@ -21,6 +21,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
var nominalVoltage: Double var nominalVoltage: Double
var capacityAmpHours: Double var capacityAmpHours: Double
var chemistry: Chemistry var chemistry: Chemistry
var iconName: String
var colorName: String
var system: ElectricalSystem var system: ElectricalSystem
init( init(
@@ -29,6 +31,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
nominalVoltage: Double = 12.8, nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100, capacityAmpHours: Double = 100,
chemistry: Chemistry = .lithiumIronPhosphate, chemistry: Chemistry = .lithiumIronPhosphate,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem system: ElectricalSystem
) { ) {
self.id = id self.id = id
@@ -36,6 +40,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.nominalVoltage = nominalVoltage self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours self.capacityAmpHours = capacityAmpHours
self.chemistry = chemistry self.chemistry = chemistry
self.iconName = iconName
self.colorName = colorName
self.system = system self.system = system
} }
@@ -45,6 +51,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
self.nominalVoltage = savedBattery.nominalVoltage self.nominalVoltage = savedBattery.nominalVoltage
self.capacityAmpHours = savedBattery.capacityAmpHours self.capacityAmpHours = savedBattery.capacityAmpHours
self.chemistry = savedBattery.chemistry self.chemistry = savedBattery.chemistry
self.iconName = savedBattery.iconName
self.colorName = savedBattery.colorName
self.system = system self.system = system
} }
@@ -57,6 +65,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
savedBattery.nominalVoltage = nominalVoltage savedBattery.nominalVoltage = nominalVoltage
savedBattery.capacityAmpHours = capacityAmpHours savedBattery.capacityAmpHours = capacityAmpHours
savedBattery.chemistry = chemistry savedBattery.chemistry = chemistry
savedBattery.iconName = iconName
savedBattery.colorName = colorName
savedBattery.system = system savedBattery.system = system
savedBattery.timestamp = Date() savedBattery.timestamp = Date()
} }
@@ -68,7 +78,9 @@ extension BatteryConfiguration {
lhs.name == rhs.name && lhs.name == rhs.name &&
lhs.nominalVoltage == rhs.nominalVoltage && lhs.nominalVoltage == rhs.nominalVoltage &&
lhs.capacityAmpHours == rhs.capacityAmpHours && lhs.capacityAmpHours == rhs.capacityAmpHours &&
lhs.chemistry == rhs.chemistry lhs.chemistry == rhs.chemistry &&
lhs.iconName == rhs.iconName &&
lhs.colorName == rhs.colorName
} }
func hash(into hasher: inout Hasher) { func hash(into hasher: inout Hasher) {
@@ -77,5 +89,7 @@ extension BatteryConfiguration {
hasher.combine(nominalVoltage) hasher.combine(nominalVoltage)
hasher.combine(capacityAmpHours) hasher.combine(capacityAmpHours)
hasher.combine(chemistry) hasher.combine(chemistry)
hasher.combine(iconName)
hasher.combine(colorName)
} }
} }

View File

@@ -5,6 +5,7 @@ struct BatteryEditorView: View {
@State private var editingField: EditingField? @State private var editingField: EditingField?
@State private var voltageInput: String = "" @State private var voltageInput: String = ""
@State private var capacityInput: String = "" @State private var capacityInput: String = ""
@State private var showingAppearanceEditor = false
let onSave: (BatteryConfiguration) -> Void let onSave: (BatteryConfiguration) -> Void
private enum EditingField { private enum EditingField {
@@ -16,6 +17,21 @@ struct BatteryEditorView: View {
private let capacitySnapValues: [Double] = [10, 20, 50, 75, 100, 125, 150, 200, 300, 400, 600, 800, 1000] private let capacitySnapValues: [Double] = [10, 20, 50, 75, 100, 125, 150, 200, 300, 400, 600, 800, 1000]
private let voltageSnapTolerance: Double = 0.5 private let voltageSnapTolerance: Double = 0.5
private let capacitySnapTolerance: Double = 10.0 private let capacitySnapTolerance: Double = 10.0
private let batteryIconOptions: [String] = [
"battery.100",
"battery.100.bolt",
"battery.75",
"battery.25",
"battery.0",
"bolt",
"bolt.fill",
"bolt.circle",
"bolt.horizontal.circle",
"powerplug",
"car.battery",
"bolt.square",
"lightbulb"
]
private var nameFieldLabel: String { private var nameFieldLabel: String {
String( String(
@@ -88,6 +104,41 @@ struct BatteryEditorView: View {
comment: "Label used for energy values" comment: "Label used for energy values"
) )
} }
private var appearanceEditorTitle: String {
NSLocalizedString(
"battery.editor.appearance.title",
bundle: .main,
value: "Battery Appearance",
comment: "Title for the battery appearance editor"
)
}
private var appearanceEditorSubtitle: String {
NSLocalizedString(
"battery.editor.appearance.subtitle",
bundle: .main,
value: "Customize how this battery shows up",
comment: "Subtitle shown in the battery appearance editor preview"
)
}
private var appearanceAccessibilityLabel: String {
NSLocalizedString(
"battery.editor.appearance.accessibility",
bundle: .main,
value: "Edit battery appearance",
comment: "Accessibility label for the battery appearance editor button"
)
}
private var iconColor: Color {
colorForName(configuration.colorName)
}
private var displayName: String {
configuration.name.isEmpty ? namePlaceholder : configuration.name
}
private var voltageSliderRange: ClosedRange<Double> { private var voltageSliderRange: ClosedRange<Double> {
let lowerBound = max(0, min(6, configuration.nominalVoltage)) let lowerBound = max(0, min(6, configuration.nominalVoltage))
@@ -107,27 +158,44 @@ struct BatteryEditorView: View {
} }
var body: some View { var body: some View {
ScrollView { List {
VStack(spacing: 24) { configurationSection
headerCard sliderSection
slidersSection
}
.padding(.vertical, 24)
.padding(.horizontal)
} }
.listStyle(.plain)
.scrollIndicators(.hidden)
.scrollContentBackground(.hidden)
.background(Color(.systemGroupedBackground).ignoresSafeArea()) .background(Color(.systemGroupedBackground).ignoresSafeArea())
.navigationTitle( .navigationTitle("")
NSLocalizedString(
"battery.editor.title",
bundle: .main,
value: "Battery Setup",
comment: "Title for the battery editor"
)
)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
navigationTitleView
}
}
.onDisappear { .onDisappear {
onSave(configuration) onSave(configuration)
} }
.sheet(isPresented: $showingAppearanceEditor) {
ItemEditorView(
title: appearanceEditorTitle,
nameFieldLabel: nameFieldLabel,
previewSubtitle: appearanceEditorSubtitle,
icons: batteryIconOptions,
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 }
)
)
}
.alert( .alert(
NSLocalizedString( NSLocalizedString(
"battery.editor.alert.voltage.title", "battery.editor.alert.voltage.title",
@@ -278,26 +346,32 @@ struct BatteryEditorView: View {
} }
} }
private var headerCard: some View { private var navigationTitleView: some View {
VStack(alignment: .leading, spacing: 16) { Button {
VStack(alignment: .leading, spacing: 8) { showingAppearanceEditor = true
Text(nameFieldLabel) } label: {
.font(.caption) HStack(spacing: 8) {
.foregroundStyle(.secondary) LoadIconView(
TextField(namePlaceholder, text: $configuration.name) remoteIconURLString: nil,
.textInputAutocapitalization(.words) fallbackSystemName: configuration.iconName.isEmpty ? "battery.100.bolt" : configuration.iconName,
.padding(.vertical, 10) fallbackColor: iconColor,
.padding(.horizontal, 12) size: 26
.background( )
RoundedRectangle(cornerRadius: 12, style: .continuous) Text(displayName)
.fill(Color(.secondarySystemBackground)) .font(.headline)
) .fontWeight(.semibold)
.foregroundColor(.primary)
} }
}
.buttonStyle(.plain)
.accessibilityLabel(appearanceAccessibilityLabel)
}
private var configurationSection: some View {
Section {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text(chemistryLabel) Text(chemistryLabel.uppercased())
.font(.caption) .font(.headline)
.foregroundStyle(.secondary)
Menu { Menu {
ForEach(BatteryConfiguration.Chemistry.allCases) { chemistry in ForEach(BatteryConfiguration.Chemistry.allCases) { chemistry in
Button { Button {
@@ -313,25 +387,29 @@ struct BatteryEditorView: View {
} label: { } label: {
HStack { HStack {
Text(configuration.chemistry.displayName) Text(configuration.chemistry.displayName)
.font(.body.weight(.semibold)) .font(.title)
.fontWeight(.bold)
Spacer() Spacer()
Image(systemName: "chevron.down") Image(systemName: "chevron.down")
.font(.footnote.weight(.bold)) .font(.footnote.weight(.bold))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
.padding(.vertical, 10)
.padding(.horizontal, 12)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(.secondarySystemBackground))
)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
.padding(.vertical, 6)
}
.listRowBackground(Color(.systemBackground))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
}
VStack(alignment: .leading, spacing: 6) { private var summarySection: some View {
Text(summaryLabel) Section {
.font(.caption) VStack(alignment: .leading, spacing: 8) {
Text(summaryLabel.uppercased())
.font(.caption2)
.fontWeight(.medium)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
ViewThatFits(in: .horizontal) { ViewThatFits(in: .horizontal) {
HStack(spacing: 16) { HStack(spacing: 16) {
@@ -371,16 +449,15 @@ struct BatteryEditorView: View {
} }
} }
} }
.padding(.vertical, 6)
} }
.padding(20) .listRowBackground(Color(.systemBackground))
.background( .listRowSeparator(.hidden)
RoundedRectangle(cornerRadius: 20, style: .continuous) .listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
.fill(Color(.tertiarySystemBackground))
)
} }
private var slidersSection: some View { private var sliderSection: some View {
VStack(spacing: 30) { Section {
SliderSection( SliderSection(
title: sliderVoltageTitle, title: sliderVoltageTitle,
value: Binding( value: Binding(
@@ -398,6 +475,7 @@ struct BatteryEditorView: View {
tapAction: beginVoltageEditing, tapAction: beginVoltageEditing,
snapValues: editingField == .voltage ? nil : voltageSnapValues snapValues: editingField == .voltage ? nil : voltageSnapValues
) )
.listRowSeparator(.hidden)
SliderSection( SliderSection(
title: sliderCapacityTitle, title: sliderCapacityTitle,
@@ -416,12 +494,10 @@ struct BatteryEditorView: View {
tapAction: beginCapacityEditing, tapAction: beginCapacityEditing,
snapValues: editingField == .capacity ? nil : capacitySnapValues snapValues: editingField == .capacity ? nil : capacitySnapValues
) )
.listRowSeparator(.hidden)
} }
.padding() .listRowBackground(Color(.systemBackground))
.background( .listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(Color(.secondarySystemBackground))
)
} }
private func normalizedVoltage(for value: Double) -> Double { private func normalizedVoltage(for value: Double) -> Double {
@@ -475,6 +551,25 @@ 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)
@@ -516,7 +611,12 @@ struct BatteryEditorView: View {
let previewSystem = ElectricalSystem(name: "Camper") let previewSystem = ElectricalSystem(name: "Camper")
return NavigationStack { return NavigationStack {
BatteryEditorView( BatteryEditorView(
configuration: BatteryConfiguration(name: "House Bank", system: previewSystem), configuration: BatteryConfiguration(
name: "House Bank",
iconName: "battery.100.bolt",
colorName: "green",
system: previewSystem
),
onSave: { _ in } onSave: { _ in }
) )
} }

View File

@@ -8,6 +8,8 @@ class SavedBattery {
var nominalVoltage: Double var nominalVoltage: Double
var capacityAmpHours: Double var capacityAmpHours: Double
private var chemistryRawValue: String private var chemistryRawValue: String
var iconName: String = "battery.100"
var colorName: String = "blue"
var system: ElectricalSystem? var system: ElectricalSystem?
var timestamp: Date var timestamp: Date
@@ -17,6 +19,8 @@ class SavedBattery {
nominalVoltage: Double = 12.8, nominalVoltage: Double = 12.8,
capacityAmpHours: Double = 100, capacityAmpHours: Double = 100,
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate, chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
iconName: String = "battery.100",
colorName: String = "blue",
system: ElectricalSystem? = nil, system: ElectricalSystem? = nil,
timestamp: Date = Date() timestamp: Date = Date()
) { ) {
@@ -25,6 +29,8 @@ class SavedBattery {
self.nominalVoltage = nominalVoltage self.nominalVoltage = nominalVoltage
self.capacityAmpHours = capacityAmpHours self.capacityAmpHours = capacityAmpHours
self.chemistryRawValue = chemistry.rawValue self.chemistryRawValue = chemistry.rawValue
self.iconName = iconName
self.colorName = colorName
self.system = system self.system = system
self.timestamp = timestamp self.timestamp = timestamp
} }

View File

@@ -100,6 +100,8 @@ struct SystemComponentsPersistence {
) )
return BatteryConfiguration( return BatteryConfiguration(
name: batteryName, name: batteryName,
iconName: "battery.100.bolt",
colorName: system.colorName,
system: system system: system
) )
} }
@@ -119,6 +121,8 @@ struct SystemComponentsPersistence {
nominalVoltage: configuration.nominalVoltage, nominalVoltage: configuration.nominalVoltage,
capacityAmpHours: configuration.capacityAmpHours, capacityAmpHours: configuration.capacityAmpHours,
chemistry: configuration.chemistry, chemistry: configuration.chemistry,
iconName: configuration.iconName,
colorName: configuration.colorName,
system: system system: system
) )
context.insert(newBattery) context.insert(newBattery)