better battery editor view
This commit is contained in:
@@ -255,7 +255,7 @@ struct BatteriesView: View {
|
||||
private func batteryRow(for battery: SavedBattery) -> some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(spacing: 12) {
|
||||
batteryIcon
|
||||
batteryIcon(for: battery)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
@@ -312,12 +312,12 @@ struct BatteriesView: View {
|
||||
)
|
||||
}
|
||||
|
||||
private var batteryIcon: some View {
|
||||
private func batteryIcon(for battery: SavedBattery) -> some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(colorForName(system.colorName))
|
||||
.fill(colorForName(battery.colorName))
|
||||
.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))
|
||||
.foregroundStyle(Color.white)
|
||||
}
|
||||
@@ -589,6 +589,8 @@ private enum BatteriesViewPreviewData {
|
||||
nominalVoltage: 12.8,
|
||||
capacityAmpHours: 200,
|
||||
chemistry: .lithiumIronPhosphate,
|
||||
iconName: "battery.100.bolt",
|
||||
colorName: "green",
|
||||
system: system
|
||||
),
|
||||
SavedBattery(
|
||||
@@ -596,6 +598,8 @@ private enum BatteriesViewPreviewData {
|
||||
nominalVoltage: 12.0,
|
||||
capacityAmpHours: 90,
|
||||
chemistry: .agm,
|
||||
iconName: "bolt",
|
||||
colorName: "orange",
|
||||
system: system
|
||||
)
|
||||
]
|
||||
|
||||
@@ -21,6 +21,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
var nominalVoltage: Double
|
||||
var capacityAmpHours: Double
|
||||
var chemistry: Chemistry
|
||||
var iconName: String
|
||||
var colorName: String
|
||||
var system: ElectricalSystem
|
||||
|
||||
init(
|
||||
@@ -29,6 +31,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
nominalVoltage: Double = 12.8,
|
||||
capacityAmpHours: Double = 100,
|
||||
chemistry: Chemistry = .lithiumIronPhosphate,
|
||||
iconName: String = "battery.100",
|
||||
colorName: String = "blue",
|
||||
system: ElectricalSystem
|
||||
) {
|
||||
self.id = id
|
||||
@@ -36,6 +40,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
self.nominalVoltage = nominalVoltage
|
||||
self.capacityAmpHours = capacityAmpHours
|
||||
self.chemistry = chemistry
|
||||
self.iconName = iconName
|
||||
self.colorName = colorName
|
||||
self.system = system
|
||||
}
|
||||
|
||||
@@ -45,6 +51,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
self.nominalVoltage = savedBattery.nominalVoltage
|
||||
self.capacityAmpHours = savedBattery.capacityAmpHours
|
||||
self.chemistry = savedBattery.chemistry
|
||||
self.iconName = savedBattery.iconName
|
||||
self.colorName = savedBattery.colorName
|
||||
self.system = system
|
||||
}
|
||||
|
||||
@@ -57,6 +65,8 @@ struct BatteryConfiguration: Identifiable, Hashable {
|
||||
savedBattery.nominalVoltage = nominalVoltage
|
||||
savedBattery.capacityAmpHours = capacityAmpHours
|
||||
savedBattery.chemistry = chemistry
|
||||
savedBattery.iconName = iconName
|
||||
savedBattery.colorName = colorName
|
||||
savedBattery.system = system
|
||||
savedBattery.timestamp = Date()
|
||||
}
|
||||
@@ -68,7 +78,9 @@ extension BatteryConfiguration {
|
||||
lhs.name == rhs.name &&
|
||||
lhs.nominalVoltage == rhs.nominalVoltage &&
|
||||
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) {
|
||||
@@ -77,5 +89,7 @@ extension BatteryConfiguration {
|
||||
hasher.combine(nominalVoltage)
|
||||
hasher.combine(capacityAmpHours)
|
||||
hasher.combine(chemistry)
|
||||
hasher.combine(iconName)
|
||||
hasher.combine(colorName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ struct BatteryEditorView: View {
|
||||
@State private var editingField: EditingField?
|
||||
@State private var voltageInput: String = ""
|
||||
@State private var capacityInput: String = ""
|
||||
@State private var showingAppearanceEditor = false
|
||||
let onSave: (BatteryConfiguration) -> Void
|
||||
|
||||
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 voltageSnapTolerance: Double = 0.5
|
||||
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 {
|
||||
String(
|
||||
@@ -88,6 +104,41 @@ struct BatteryEditorView: View {
|
||||
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> {
|
||||
let lowerBound = max(0, min(6, configuration.nominalVoltage))
|
||||
@@ -107,27 +158,44 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 24) {
|
||||
headerCard
|
||||
slidersSection
|
||||
}
|
||||
.padding(.vertical, 24)
|
||||
.padding(.horizontal)
|
||||
List {
|
||||
configurationSection
|
||||
sliderSection
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollIndicators(.hidden)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color(.systemGroupedBackground).ignoresSafeArea())
|
||||
.navigationTitle(
|
||||
NSLocalizedString(
|
||||
"battery.editor.title",
|
||||
bundle: .main,
|
||||
value: "Battery Setup",
|
||||
comment: "Title for the battery editor"
|
||||
)
|
||||
)
|
||||
.navigationTitle("")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
navigationTitleView
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
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(
|
||||
NSLocalizedString(
|
||||
"battery.editor.alert.voltage.title",
|
||||
@@ -278,26 +346,32 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var headerCard: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(nameFieldLabel)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField(namePlaceholder, text: $configuration.name)
|
||||
.textInputAutocapitalization(.words)
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(.secondarySystemBackground))
|
||||
)
|
||||
private var navigationTitleView: some View {
|
||||
Button {
|
||||
showingAppearanceEditor = true
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
LoadIconView(
|
||||
remoteIconURLString: nil,
|
||||
fallbackSystemName: configuration.iconName.isEmpty ? "battery.100.bolt" : configuration.iconName,
|
||||
fallbackColor: iconColor,
|
||||
size: 26
|
||||
)
|
||||
Text(displayName)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(appearanceAccessibilityLabel)
|
||||
}
|
||||
|
||||
private var configurationSection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(chemistryLabel)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(chemistryLabel.uppercased())
|
||||
.font(.headline)
|
||||
Menu {
|
||||
ForEach(BatteryConfiguration.Chemistry.allCases) { chemistry in
|
||||
Button {
|
||||
@@ -313,25 +387,29 @@ struct BatteryEditorView: View {
|
||||
} label: {
|
||||
HStack {
|
||||
Text(configuration.chemistry.displayName)
|
||||
.font(.body.weight(.semibold))
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.footnote.weight(.bold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 12)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(Color(.secondarySystemBackground))
|
||||
)
|
||||
}
|
||||
.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) {
|
||||
Text(summaryLabel)
|
||||
.font(.caption)
|
||||
private var summarySection: some View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(summaryLabel.uppercased())
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
ViewThatFits(in: .horizontal) {
|
||||
HStack(spacing: 16) {
|
||||
@@ -371,16 +449,15 @@ struct BatteryEditorView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
.padding(20)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(Color(.tertiarySystemBackground))
|
||||
)
|
||||
.listRowBackground(Color(.systemBackground))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
|
||||
}
|
||||
|
||||
private var slidersSection: some View {
|
||||
VStack(spacing: 30) {
|
||||
private var sliderSection: some View {
|
||||
Section {
|
||||
SliderSection(
|
||||
title: sliderVoltageTitle,
|
||||
value: Binding(
|
||||
@@ -398,6 +475,7 @@ struct BatteryEditorView: View {
|
||||
tapAction: beginVoltageEditing,
|
||||
snapValues: editingField == .voltage ? nil : voltageSnapValues
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
SliderSection(
|
||||
title: sliderCapacityTitle,
|
||||
@@ -416,12 +494,10 @@ struct BatteryEditorView: View {
|
||||
tapAction: beginCapacityEditing,
|
||||
snapValues: editingField == .capacity ? nil : capacitySnapValues
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
}
|
||||
.padding()
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
||||
.fill(Color(.secondarySystemBackground))
|
||||
)
|
||||
.listRowBackground(Color(.systemBackground))
|
||||
.listRowInsets(EdgeInsets(top: 12, leading: 18, bottom: 12, trailing: 18))
|
||||
}
|
||||
|
||||
private func normalizedVoltage(for value: Double) -> Double {
|
||||
@@ -475,6 +551,25 @@ struct BatteryEditorView: View {
|
||||
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 {
|
||||
VStack(spacing: 4) {
|
||||
Image(systemName: symbol)
|
||||
@@ -516,7 +611,12 @@ struct BatteryEditorView: View {
|
||||
let previewSystem = ElectricalSystem(name: "Camper")
|
||||
return NavigationStack {
|
||||
BatteryEditorView(
|
||||
configuration: BatteryConfiguration(name: "House Bank", system: previewSystem),
|
||||
configuration: BatteryConfiguration(
|
||||
name: "House Bank",
|
||||
iconName: "battery.100.bolt",
|
||||
colorName: "green",
|
||||
system: previewSystem
|
||||
),
|
||||
onSave: { _ in }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ class SavedBattery {
|
||||
var nominalVoltage: Double
|
||||
var capacityAmpHours: Double
|
||||
private var chemistryRawValue: String
|
||||
var iconName: String = "battery.100"
|
||||
var colorName: String = "blue"
|
||||
var system: ElectricalSystem?
|
||||
var timestamp: Date
|
||||
|
||||
@@ -17,6 +19,8 @@ class SavedBattery {
|
||||
nominalVoltage: Double = 12.8,
|
||||
capacityAmpHours: Double = 100,
|
||||
chemistry: BatteryConfiguration.Chemistry = .lithiumIronPhosphate,
|
||||
iconName: String = "battery.100",
|
||||
colorName: String = "blue",
|
||||
system: ElectricalSystem? = nil,
|
||||
timestamp: Date = Date()
|
||||
) {
|
||||
@@ -25,6 +29,8 @@ class SavedBattery {
|
||||
self.nominalVoltage = nominalVoltage
|
||||
self.capacityAmpHours = capacityAmpHours
|
||||
self.chemistryRawValue = chemistry.rawValue
|
||||
self.iconName = iconName
|
||||
self.colorName = colorName
|
||||
self.system = system
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@ struct SystemComponentsPersistence {
|
||||
)
|
||||
return BatteryConfiguration(
|
||||
name: batteryName,
|
||||
iconName: "battery.100.bolt",
|
||||
colorName: system.colorName,
|
||||
system: system
|
||||
)
|
||||
}
|
||||
@@ -119,6 +121,8 @@ struct SystemComponentsPersistence {
|
||||
nominalVoltage: configuration.nominalVoltage,
|
||||
capacityAmpHours: configuration.capacityAmpHours,
|
||||
chemistry: configuration.chemistry,
|
||||
iconName: configuration.iconName,
|
||||
colorName: configuration.colorName,
|
||||
system: system
|
||||
)
|
||||
context.insert(newBattery)
|
||||
|
||||
Reference in New Issue
Block a user