Add standalone wiring diagram PNG export

Fetch the wiring diagram from the VoltPlan API and share it as a
standalone PNG from the Overview share menu on both platforms, with a
localized error when the diagram cannot be generated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 01:04:11 +02:00
parent 67ec44e60a
commit d97e3a2b7c
10 changed files with 350 additions and 58 deletions

View File

@@ -19,7 +19,7 @@ struct LoadsView: View {
@State private var showingSystemEditor = false
@State private var hasPresentedSystemEditorOnAppear = false
@State private var hasOpenedLoadOnAppear = false
@State private var showingComponentLibrary = false
@State private var activeLibrary: ComponentLibraryType?
@State private var showingSystemBOM = false
@State private var selectedComponentTab: ComponentTab
@State private var batteryDraft: BatteryConfiguration?
@@ -86,23 +86,7 @@ struct LoadsView: View {
.accessibilityIdentifier("components-tab")
}
Group {
if savedBatteries.isEmpty {
OnboardingInfoView(
configuration: .battery(),
onPrimaryAction: { startBatteryConfiguration() }
)
} else {
BatteriesView(
system: system,
batteries: savedBatteries,
editMode: $editMode,
onEdit: { editBattery($0) },
onDelete: deleteBatteries
)
.environment(\.editMode, $editMode)
}
}
batteriesTab
.tag(ComponentTab.batteries)
.tabItem {
Label(
@@ -117,14 +101,7 @@ struct LoadsView: View {
}
.environment(\.editMode, $editMode)
ChargersView(
system: system,
chargers: savedChargers,
editMode: $editMode,
onAdd: { startChargerConfiguration() },
onEdit: { editCharger($0) },
onDelete: deleteChargers
)
chargersTab
.tag(ComponentTab.chargers)
.tabItem {
Label(
@@ -272,9 +249,9 @@ struct LoadsView: View {
exportDiagramImage()
}
}
.sheet(isPresented: $showingComponentLibrary) {
ComponentLibraryView { item in
addComponent(item)
.sheet(item: $activeLibrary) { type in
ComponentLibraryView(libraryType: type) { item in
handleLibrarySelection(item, for: type)
}
}
.sheet(isPresented: $showingSystemBOM) {
@@ -439,9 +416,9 @@ struct LoadsView: View {
}
}
private var libraryButton: some View {
private func libraryButton(type: ComponentLibraryType) -> some View {
Button {
openComponentLibrary(source: "library-button")
openComponentLibrary(source: "library-button", type: type)
} label: {
Group {
if #available(iOS 26.0, *) {
@@ -490,6 +467,53 @@ struct LoadsView: View {
.background(Color(.systemGroupedBackground))
}
private var batteriesTab: some View {
Group {
if savedBatteries.isEmpty {
OnboardingInfoView(
configuration: .battery(),
onPrimaryAction: { startBatteryConfiguration() },
onSecondaryAction: { openComponentLibrary(source: "batteries-onboarding", type: .battery) }
)
} else {
BatteriesView(
system: system,
batteries: savedBatteries,
editMode: $editMode,
onEdit: { editBattery($0) },
onDelete: deleteBatteries
)
.environment(\.editMode, $editMode)
}
}
.overlay(alignment: .bottomTrailing) {
if !savedBatteries.isEmpty {
libraryButton(type: .battery)
.padding(.trailing, 24)
.padding(.bottom, 24)
}
}
}
private var chargersTab: some View {
ChargersView(
system: system,
chargers: savedChargers,
editMode: $editMode,
onAdd: { startChargerConfiguration() },
onEdit: { editCharger($0) },
onDelete: deleteChargers,
onBrowseLibrary: { openComponentLibrary(source: "chargers-onboarding", type: .charger) }
)
.overlay(alignment: .bottomTrailing) {
if !savedChargers.isEmpty {
libraryButton(type: .charger)
.padding(.trailing, 24)
.padding(.bottom, 24)
}
}
}
@ViewBuilder
private var loadsListWithHeader: some View {
Group {
@@ -507,7 +531,7 @@ struct LoadsView: View {
}
}
.overlay(alignment: .bottomTrailing) {
libraryButton
libraryButton(type: .load)
.padding(.trailing, 24)
.padding(.bottom, 24)
}
@@ -802,15 +826,63 @@ struct LoadsView: View {
showingSystemEditor = true
}
private func openComponentLibrary(source: String) {
private func openComponentLibrary(source: String, type: ComponentLibraryType = .load) {
AnalyticsTracker.log(
"Component Library Opened",
properties: [
"source": source,
"type": type.rawValue,
"system": system.name
]
)
showingComponentLibrary = true
activeLibrary = type
}
private func handleLibrarySelection(_ item: ComponentLibraryItem, for type: ComponentLibraryType) {
switch type {
case .load:
addComponent(item)
case .battery:
addBatteryFromLibrary(item)
case .charger:
addChargerFromLibrary(item)
}
}
private func addBatteryFromLibrary(_ item: ComponentLibraryItem) {
AnalyticsTracker.log(
"Library Battery Added",
properties: [
"id": item.id,
"name": item.localizedName,
"system": system.name
]
)
batteryDraft = SystemComponentsPersistence.makeBatteryDraft(
from: item,
for: system,
existingLoads: savedLoads,
existingBatteries: savedBatteries,
existingChargers: savedChargers
)
}
private func addChargerFromLibrary(_ item: ComponentLibraryItem) {
AnalyticsTracker.log(
"Library Charger Added",
properties: [
"id": item.id,
"name": item.localizedName,
"system": system.name
]
)
chargerDraft = SystemComponentsPersistence.makeChargerDraft(
from: item,
for: system,
existingLoads: savedLoads,
existingBatteries: savedBatteries,
existingChargers: savedChargers
)
}
private func openBillOfMaterials() {