From 802b111aa7dc871d323f2de563454707ec12bf12 Mon Sep 17 00:00:00 2001 From: Stefan Lange-Hegermann Date: Wed, 22 Oct 2025 17:17:57 +0200 Subject: [PATCH] onboarding buttons in the system overview --- Cable/Base.lproj/Localizable.strings | 4 + Cable/BatteryEditorView.swift | 1 - Cable/LoadsView.swift | 5 +- Cable/SystemOverviewView.swift | 285 +++++++++++++++++---------- Cable/SystemsView.swift | 12 ++ Cable/de.lproj/Localizable.strings | 4 + Cable/es.lproj/Localizable.strings | 4 + Cable/fr.lproj/Localizable.strings | 4 + Cable/nl.lproj/Localizable.strings | 4 + Shots/Titles/de.conf | 2 +- 10 files changed, 220 insertions(+), 105 deletions(-) diff --git a/Cable/Base.lproj/Localizable.strings b/Cable/Base.lproj/Localizable.strings index 3189797..595498b 100644 --- a/Cable/Base.lproj/Localizable.strings +++ b/Cable/Base.lproj/Localizable.strings @@ -77,6 +77,9 @@ "loads.overview.metric.count" = "Loads"; "loads.overview.metric.current" = "Total Current"; "loads.overview.metric.power" = "Total Power"; +"loads.overview.empty.message" = "Start by adding a load to see system insights."; +"loads.overview.empty.create" = "Add Load"; +"loads.overview.empty.library" = "Browse Library"; "loads.overview.status.missing_details.title" = "Missing load details"; "loads.overview.status.missing_details.message" = "Enter cable length and wire size for %d %@ to see accurate recommendations."; "loads.overview.status.missing_details.singular" = "load"; @@ -98,6 +101,7 @@ "battery.bank.metric.count" = "Batteries"; "battery.bank.metric.capacity" = "Capacity"; "battery.bank.metric.energy" = "Energy"; +"battery.overview.empty.create" = "Add Battery"; "battery.bank.badge.voltage" = "Voltage"; "battery.bank.badge.capacity" = "Capacity"; "battery.bank.badge.energy" = "Energy"; diff --git a/Cable/BatteryEditorView.swift b/Cable/BatteryEditorView.swift index f8d8ed5..4ae7ec1 100644 --- a/Cable/BatteryEditorView.swift +++ b/Cable/BatteryEditorView.swift @@ -162,7 +162,6 @@ struct BatteryEditorView: View { headerInfoBar List { configurationSection - summarySection sliderSection } .listStyle(.plain) diff --git a/Cable/LoadsView.swift b/Cable/LoadsView.swift index 67f7990..446629d 100644 --- a/Cable/LoadsView.swift +++ b/Cable/LoadsView.swift @@ -252,7 +252,10 @@ struct LoadsView: View { loads: savedLoads, batteries: savedBatteries, onSelectLoads: { selectedComponentTab = .components }, - onSelectBatteries: { selectedComponentTab = .batteries } + onSelectBatteries: { selectedComponentTab = .batteries }, + onCreateLoad: { createNewLoad() }, + onBrowseLibrary: { showingComponentLibrary = true }, + onCreateBattery: { startBatteryConfiguration() } ) .accessibilityIdentifier("system-overview") } diff --git a/Cable/SystemOverviewView.swift b/Cable/SystemOverviewView.swift index ef2849a..c64aee2 100644 --- a/Cable/SystemOverviewView.swift +++ b/Cable/SystemOverviewView.swift @@ -8,6 +8,9 @@ struct SystemOverviewView: View { let batteries: [SavedBattery] let onSelectLoads: () -> Void let onSelectBatteries: () -> Void + let onCreateLoad: () -> Void + let onBrowseLibrary: () -> Void + let onCreateBattery: () -> Void var body: some View { ScrollView { @@ -95,14 +98,9 @@ struct SystemOverviewView: View { ) } + @ViewBuilder private var loadsCard: some View { - Button { - if suppressLoadNavigation { - suppressLoadNavigation = false - return - } - onSelectLoads() - } label: { + if loads.isEmpty { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .firstTextBaseline) { Text(loadsSummaryTitle) @@ -110,15 +108,53 @@ struct SystemOverviewView: View { Spacer() } - if loads.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text(loadsEmptyTitle) - .font(.subheadline.weight(.semibold)) - Text(loadsEmptySubtitle) - .font(.footnote) - .foregroundStyle(.secondary) + VStack(alignment: .leading, spacing: 8) { + Text(loadsEmptyTitle) + .font(.subheadline.weight(.semibold)) + Text(loadsEmptyMessage) + .font(.footnote) + .foregroundStyle(.secondary) + } + + VStack(alignment: .leading, spacing: 10) { + Button(action: onCreateLoad) { + Label(loadsEmptyCreateAction, systemImage: "plus") + .frame(maxWidth: .infinity) } - } else { + .buttonStyle(.borderedProminent) + .controlSize(.large) + + Button(action: onBrowseLibrary) { + Label(loadsEmptyBrowseAction, systemImage: "books.vertical") + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.accentColor) + .controlSize(.large) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 18) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(Color(.systemBackground)) + ) + } else { + Button { + if suppressLoadNavigation { + suppressLoadNavigation = false + return + } + onSelectLoads() + } label: { + VStack(alignment: .leading, spacing: 16) { + HStack(alignment: .firstTextBaseline) { + Text(loadsSummaryTitle) + .font(.headline.weight(.semibold)) + Spacer() + } + ViewThatFits(in: .horizontal) { HStack(spacing: 16) { summaryMetric( @@ -173,32 +209,32 @@ struct SystemOverviewView: View { ) } } + .padding(.horizontal, 16) + .padding(.vertical, 18) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(Color(.systemBackground)) + ) } - .padding(.horizontal, 16) - .padding(.vertical, 18) - .frame(maxWidth: .infinity, alignment: .leading) - .background( - RoundedRectangle(cornerRadius: 20, style: .continuous) - .fill(Color(.systemBackground)) - ) - } - .buttonStyle(.plain) - .alert(item: $activeStatus) { status in - let detail = status.detailInfo() - return Alert( - title: Text(detail.title), - message: Text(detail.message), - dismissButton: .default( - Text( - NSLocalizedString( - "battery.bank.status.dismiss", - bundle: .main, - value: "Got it", - comment: "Dismiss button title for load status alert" + .buttonStyle(.plain) + .alert(item: $activeStatus) { status in + let detail = status.detailInfo() + return Alert( + title: Text(detail.title), + message: Text(detail.message), + dismissButton: .default( + Text( + NSLocalizedString( + "battery.bank.status.dismiss", + bundle: .main, + value: "Got it", + comment: "Dismiss button title for load status alert" + ) ) ) ) - ) + } } } @@ -210,78 +246,87 @@ struct SystemOverviewView: View { .font(.headline.weight(.semibold)) if let warning = batteryWarning { HStack(spacing: 6) { - Image(systemName: warning.symbol) - .font(.system(size: 14, weight: .semibold)) - .foregroundStyle(warning.tint) - Text(warning.shortLabel) - .font(.caption.weight(.semibold)) - .foregroundStyle(warning.tint) - } - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(warning.tint.opacity(0.12)) - ) - } - Spacer() - } - - if batteries.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text(batteryEmptyTitle) - .font(.subheadline.weight(.semibold)) - Text(batteryEmptySubtitle) - .font(.footnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - } else { - ViewThatFits(in: .horizontal) { - HStack(spacing: 16) { - summaryMetric( - icon: "battery.100", - label: batteryCountLabel, - value: "\(batteries.count)", - tint: .blue - ) - summaryMetric( - icon: "gauge.medium", - label: batteryCapacityLabel, - value: formattedValue(totalCapacity, unit: "Ah"), - tint: .orange - ) - summaryMetric( - icon: "bolt.circle", - label: batteryEnergyLabel, - value: formattedValue(totalEnergy, unit: "Wh"), - tint: .green + Image(systemName: warning.symbol) + .font(.system(size: 14, weight: .semibold)) + .foregroundStyle(warning.tint) + Text(warning.shortLabel) + .font(.caption.weight(.semibold)) + .foregroundStyle(warning.tint) + } + .padding(.horizontal, 10) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(warning.tint.opacity(0.12)) ) } + Spacer() + } + if batteries.isEmpty { VStack(alignment: .leading, spacing: 12) { - summaryMetric( - icon: "battery.100", - label: batteryCountLabel, - value: "\(batteries.count)", - tint: .blue - ) - summaryMetric( - icon: "gauge.medium", - label: batteryCapacityLabel, - value: formattedValue(totalCapacity, unit: "Ah"), - tint: .orange - ) - summaryMetric( - icon: "bolt.circle", - label: batteryEnergyLabel, - value: formattedValue(totalEnergy, unit: "Wh"), - tint: .green - ) + VStack(alignment: .leading, spacing: 4) { + Text(batteryEmptyTitle) + .font(.subheadline.weight(.semibold)) + Text(batteryEmptySubtitle) + .font(.footnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + + Button(action: onCreateBattery) { + Label(batteryEmptyCreateAction, systemImage: "plus") + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .controlSize(.large) + } + } else { + ViewThatFits(in: .horizontal) { + HStack(spacing: 16) { + summaryMetric( + icon: "battery.100", + label: batteryCountLabel, + value: "\(batteries.count)", + tint: .blue + ) + summaryMetric( + icon: "gauge.medium", + label: batteryCapacityLabel, + value: formattedValue(totalCapacity, unit: "Ah"), + tint: .orange + ) + summaryMetric( + icon: "bolt.circle", + label: batteryEnergyLabel, + value: formattedValue(totalEnergy, unit: "Wh"), + tint: .green + ) + } + + VStack(alignment: .leading, spacing: 12) { + summaryMetric( + icon: "battery.100", + label: batteryCountLabel, + value: "\(batteries.count)", + tint: .blue + ) + summaryMetric( + icon: "gauge.medium", + label: batteryCapacityLabel, + value: formattedValue(totalCapacity, unit: "Ah"), + tint: .orange + ) + summaryMetric( + icon: "bolt.circle", + label: batteryEnergyLabel, + value: formattedValue(totalEnergy, unit: "Wh"), + tint: .green + ) + } } } } - } .padding(.horizontal, 16) .padding(.vertical, 18) .frame(maxWidth: .infinity, alignment: .leading) @@ -540,6 +585,33 @@ struct SystemOverviewView: View { ) } + private var loadsEmptyMessage: String { + NSLocalizedString( + "loads.overview.empty.message", + bundle: .main, + value: "Start by adding a load to see system insights.", + comment: "Message shown when no loads exist" + ) + } + + private var loadsEmptyCreateAction: String { + NSLocalizedString( + "loads.overview.empty.create", + bundle: .main, + value: "Create Load", + comment: "Button title to create a new load" + ) + } + + private var loadsEmptyBrowseAction: String { + NSLocalizedString( + "loads.overview.empty.library", + bundle: .main, + value: "Browse Library", + comment: "Button title to open load library" + ) + } + private var batterySummaryTitle: String { NSLocalizedString( "battery.bank.header.title", @@ -596,6 +668,15 @@ struct SystemOverviewView: View { return String(format: format, system.name) } + private var batteryEmptyCreateAction: String { + NSLocalizedString( + "battery.overview.empty.create", + bundle: .main, + value: "Create Battery", + comment: "Button title to create a new battery" + ) + } + private var systemOverviewTitle: String { NSLocalizedString( "overview.system.header.title", diff --git a/Cable/SystemsView.swift b/Cable/SystemsView.swift index 5fd80e6..ad52e94 100644 --- a/Cable/SystemsView.swift +++ b/Cable/SystemsView.swift @@ -17,6 +17,7 @@ struct SystemsView: View { @State private var systemNavigationTarget: SystemNavigationTarget? @State private var showingComponentLibrary = false @State private var showingSettings = false + @State private var hasPerformedInitialAutoNavigation = false private let systemColorOptions = [ "blue", "green", "orange", "red", "purple", "yellow", @@ -140,6 +141,9 @@ struct SystemsView: View { ) } } + .onAppear { + performInitialAutoNavigationIfNeeded() + } .sheet(isPresented: $showingComponentLibrary) { ComponentLibraryView { item in addComponentFromLibrary(item) @@ -219,6 +223,14 @@ struct SystemsView: View { return newSystem } + private func performInitialAutoNavigationIfNeeded() { + guard !hasPerformedInitialAutoNavigation else { return } + hasPerformedInitialAutoNavigation = true + + guard systems.count == 1, let system = systems.first else { return } + navigateToSystem(system, presentSystemEditor: false, loadToOpen: nil, animated: false) + } + private func addComponentFromLibrary(_ item: ComponentLibraryItem) { let system = makeSystem() let load = createLoad(from: item, in: system) diff --git a/Cable/de.lproj/Localizable.strings b/Cable/de.lproj/Localizable.strings index a71c3d3..dd68338 100644 --- a/Cable/de.lproj/Localizable.strings +++ b/Cable/de.lproj/Localizable.strings @@ -145,6 +145,9 @@ "loads.overview.metric.count" = "Verbraucher"; "loads.overview.metric.current" = "Strom"; "loads.overview.metric.power" = "Leistung"; +"loads.overview.empty.message" = "Füge einen Verbraucher hinzu, um dein System einzurichten."; +"loads.overview.empty.create" = "Verbraucher hinzufügen"; +"loads.overview.empty.library" = "Bibliothek durchsuchen"; "loads.overview.status.missing_details.title" = "Fehlende Verbraucherdetails"; "loads.overview.status.missing_details.message" = "Gib Kabellänge und Leitungsquerschnitt für %d %@ ein, um genaue Empfehlungen zu erhalten."; "loads.overview.status.missing_details.singular" = "Verbraucher"; @@ -166,6 +169,7 @@ "battery.bank.metric.count" = "Batterien"; "battery.bank.metric.capacity" = "Kapazität"; "battery.bank.metric.energy" = "Energie"; +"battery.overview.empty.create" = "Batterie hinzufügen"; "battery.bank.badge.voltage" = "Spannung"; "battery.bank.badge.capacity" = "Kapazität"; "battery.bank.badge.energy" = "Energie"; diff --git a/Cable/es.lproj/Localizable.strings b/Cable/es.lproj/Localizable.strings index 71c8577..b30a8c3 100644 --- a/Cable/es.lproj/Localizable.strings +++ b/Cable/es.lproj/Localizable.strings @@ -144,6 +144,9 @@ "loads.overview.metric.count" = "Cargas"; "loads.overview.metric.current" = "Corriente total"; "loads.overview.metric.power" = "Potencia total"; +"loads.overview.empty.message" = "Añade una carga para ver los detalles del sistema."; +"loads.overview.empty.create" = "Añadir carga"; +"loads.overview.empty.library" = "Explorar biblioteca"; "loads.overview.status.missing_details.title" = "Faltan detalles de la carga"; "loads.overview.status.missing_details.message" = "Introduce la longitud del cable y el calibre del conductor para %d %@ para obtener recomendaciones precisas."; "loads.overview.status.missing_details.singular" = "carga"; @@ -165,6 +168,7 @@ "battery.bank.metric.count" = "Baterías"; "battery.bank.metric.capacity" = "Capacidad"; "battery.bank.metric.energy" = "Energía"; +"battery.overview.empty.create" = "Añadir batería"; "battery.bank.badge.voltage" = "Voltaje"; "battery.bank.badge.capacity" = "Capacidad"; "battery.bank.badge.energy" = "Energía"; diff --git a/Cable/fr.lproj/Localizable.strings b/Cable/fr.lproj/Localizable.strings index 52dd4f0..4c0466a 100644 --- a/Cable/fr.lproj/Localizable.strings +++ b/Cable/fr.lproj/Localizable.strings @@ -144,6 +144,9 @@ "loads.overview.metric.count" = "Charges"; "loads.overview.metric.current" = "Courant total"; "loads.overview.metric.power" = "Puissance totale"; +"loads.overview.empty.message" = "Ajoutez une charge pour voir les informations du système."; +"loads.overview.empty.create" = "Ajouter une charge"; +"loads.overview.empty.library" = "Parcourir la bibliothèque"; "loads.overview.status.missing_details.title" = "Détails de charge manquants"; "loads.overview.status.missing_details.message" = "Saisissez la longueur de câble et la section du conducteur pour %d %@ afin d'obtenir des recommandations précises."; "loads.overview.status.missing_details.singular" = "charge"; @@ -165,6 +168,7 @@ "battery.bank.metric.count" = "Batteries"; "battery.bank.metric.capacity" = "Capacité"; "battery.bank.metric.energy" = "Énergie"; +"battery.overview.empty.create" = "Ajouter une batterie"; "battery.bank.badge.voltage" = "Tension"; "battery.bank.badge.capacity" = "Capacité"; "battery.bank.badge.energy" = "Énergie"; diff --git a/Cable/nl.lproj/Localizable.strings b/Cable/nl.lproj/Localizable.strings index c87aece..fbe290e 100644 --- a/Cable/nl.lproj/Localizable.strings +++ b/Cable/nl.lproj/Localizable.strings @@ -144,6 +144,9 @@ "loads.overview.metric.count" = "Lasten"; "loads.overview.metric.current" = "Totale stroom"; "loads.overview.metric.power" = "Totaal vermogen"; +"loads.overview.empty.message" = "Voeg een belasting toe om systeeminformatie te zien."; +"loads.overview.empty.create" = "Belasting toevoegen"; +"loads.overview.empty.library" = "Bibliotheek bekijken"; "loads.overview.status.missing_details.title" = "Ontbrekende lastdetails"; "loads.overview.status.missing_details.message" = "Voer kabellengte en kabeldoorsnede in voor %d %@ om nauwkeurige aanbevelingen te krijgen."; "loads.overview.status.missing_details.singular" = "last"; @@ -165,6 +168,7 @@ "battery.bank.metric.count" = "Batterijen"; "battery.bank.metric.capacity" = "Capaciteit"; "battery.bank.metric.energy" = "Energie"; +"battery.overview.empty.create" = "Accu toevoegen"; "battery.bank.badge.voltage" = "Spanning"; "battery.bank.badge.capacity" = "Capaciteit"; "battery.bank.badge.energy" = "Energie"; diff --git a/Shots/Titles/de.conf b/Shots/Titles/de.conf index 82e85a3..754c1bb 100644 --- a/Shots/Titles/de.conf +++ b/Shots/Titles/de.conf @@ -4,4 +4,4 @@ LoadEditorView=Berechne*zuverlässig*\ndie richtige Sicherung ComponentSelectorView=Finde im*großen*Teilekatalog\nwas du suchst SystemsWithSampleData=Navigiere*schnell*\ndurch Deine Systeme AdventureVanLoads=Erstelle*individuelle*\nVerbraucher für Dein System -AdventureVanBillOfMaterials=Behalte den*Überblick*\nwas du schon gekauft hast \ No newline at end of file +AdventureVanBillOfMaterials=Behalte den*Überblick*\nwelche Teile du schon hast \ No newline at end of file