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() {

View File

@@ -75,10 +75,12 @@ object PdfShare {
return file
}
fun share(context: Context, file: File) {
fun share(context: Context, file: File) = shareFile(context, file, "application/pdf")
fun shareFile(context: Context, file: File, mimeType: String) {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "application/pdf"
type = mimeType
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

View File

@@ -0,0 +1,133 @@
package app.voltplan.cable.pdf
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.model.effectivePowerWatts
import app.voltplan.cable.data.model.energyWattHours
import app.voltplan.cable.data.model.sourceType
import app.voltplan.cable.data.UnitSystem
import app.voltplan.cable.ui.system.DetailState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.util.concurrent.TimeUnit
/**
* Fetches the system wiring diagram PNG from the VoltPlan diagram API — the same endpoint and
* payload the iOS app uses (`SystemOverviewPDFExporter.fetchDiagramImage`). Used both for the
* standalone "Wiring Diagram" image export and the diagram page embedded in the overview PDF.
*/
object SystemDiagram {
private const val ENDPOINT = "https://voltplan.app/api/diagram/generate"
private val JSON_MEDIA = "application/json; charset=utf-8".toMediaType()
private val client = OkHttpClient.Builder()
.callTimeout(15, TimeUnit.SECONDS)
.build()
/** Fetches the diagram as a [Bitmap], or null on any network/decoding failure. */
suspend fun fetch(state: DetailState, unit: UnitSystem): Bitmap? = withContext(Dispatchers.IO) {
val payload = buildPayload(state, unit)
val request = Request.Builder()
.url(ENDPOINT)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "image/png")
.post(Json.encodeToString(JsonObject.serializer(), payload).toRequestBody(JSON_MEDIA))
.build()
runCatching {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@use null
response.body?.bytes()?.let { BitmapFactory.decodeByteArray(it, 0, it.size) }
}
}.getOrNull()
}
/** Fetches the diagram, flattens it onto a white background, and opens the Android share sheet. */
suspend fun exportAndShare(
context: Context,
state: DetailState,
unit: UnitSystem,
onError: () -> Unit,
) {
val bitmap = fetch(state, unit)
if (bitmap == null) {
withContext(Dispatchers.Main) { onError() }
return
}
val file = withContext(Dispatchers.IO) {
val opaque = flattenOnWhite(bitmap)
val name = state.system?.name?.takeIf { it.isNotBlank() } ?: "System"
val dir = File(context.cacheDir, "exports").apply { mkdirs() }
val out = File(dir, "${name.replace(Regex("[^A-Za-z0-9-_]"), "_")}-Diagram.png")
out.outputStream().use { opaque.compress(Bitmap.CompressFormat.PNG, 100, it) }
out
}
withContext(Dispatchers.Main) {
Analytics.log("Diagram Image Shared", mapOf("system" to (state.system?.name ?: "")))
PdfShare.shareFile(context, file, "image/png")
}
}
private fun flattenOnWhite(source: Bitmap): Bitmap {
val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
Canvas(result).apply {
drawColor(Color.WHITE)
drawBitmap(source, 0f, 0f, null)
}
return result
}
private fun buildPayload(state: DetailState, unit: UnitSystem): JsonObject = buildJsonObject {
put("systemName", state.system?.name ?: "System")
put("source", "cable")
put("unitSystem", if (unit == UnitSystem.METRIC) "metric" else "imperial")
put("loads", buildJsonArray {
state.loads.forEach { load ->
add(buildJsonObject {
put("name", load.name)
put("power", load.power)
put("voltage", load.voltage)
put("current", load.current)
load.remoteIconURLString?.let { put("iconUrl", it) }
})
}
})
put("batteries", buildJsonArray {
state.batteries.forEach { battery ->
add(buildJsonObject {
put("name", battery.name)
put("voltage", battery.nominalVoltage)
put("capacityAh", battery.capacityAmpHours)
put("energyWh", battery.energyWattHours)
})
}
})
put("chargers", buildJsonArray {
state.chargers.forEach { charger ->
add(buildJsonObject {
put("name", charger.name)
put("inputVoltage", charger.inputVoltage)
put("outputVoltage", charger.outputVoltage)
put("power", charger.effectivePowerWatts)
put("sourceType", charger.sourceType.rawValue)
charger.remoteIconURLString?.let { put("iconUrl", it) }
})
}
})
}
}

View File

@@ -1,7 +1,11 @@
package app.voltplan.cable.pdf
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.pdf.PdfDocument
import app.voltplan.cable.R
import app.voltplan.cable.calc.ElectricalCalculations
@@ -23,6 +27,8 @@ private val ACCENT = Color.rgb(115, 87, 219)
/** Renders a full system overview PDF and opens the Android share sheet. */
object SystemOverviewPdf {
suspend fun exportAndShare(context: Context, state: DetailState, unit: UnitSystem) {
// Fetch the wiring diagram first (falls back to no diagram page if unavailable).
val diagram = SystemDiagram.fetch(state, unit)
val file = withContext(Dispatchers.IO) {
val doc = PdfDocument()
val w = PdfWriter(doc)
@@ -42,6 +48,9 @@ object SystemOverviewPdf {
summaryLine(w, context.getString(R.string.overview_pdf_summary_batterycapacity), "${Fmt.number(m.totalCapacity)} Ah")
summaryLine(w, context.getString(R.string.overview_pdf_summary_chargerpower), "${Fmt.number(m.totalChargerPower)} W")
// Full-page wiring diagram, followed by a fresh page for the entity tables.
diagram?.let { drawDiagramPage(w, it); w.beginPage() }
if (state.loads.isNotEmpty()) {
w.gap(12f); w.text(context.getString(R.string.overview_pdf_loads_section), 18f, ACCENT, bold = true); w.divider()
state.loads.forEach { load ->
@@ -89,4 +98,23 @@ object SystemOverviewPdf {
private fun summaryLine(w: PdfWriter, label: String, value: String) {
w.text("$label: $value", 12f, Color.DKGRAY)
}
/** Draws the diagram bitmap on its own page, scaled to fit the margins while keeping aspect ratio. */
private fun drawDiagramPage(w: PdfWriter, diagram: Bitmap) {
w.beginPage()
val availableWidth = PAGE_W - MARGIN * 2
val availableHeight = PAGE_H - MARGIN * 2 - 30 // leave room for footer
val imageAspect = diagram.width.toFloat() / diagram.height.toFloat()
val rectAspect = availableWidth / availableHeight
val dest = if (imageAspect > rectAspect) {
val drawHeight = availableWidth / imageAspect
RectF(MARGIN, MARGIN + (availableHeight - drawHeight) / 2f, MARGIN + availableWidth, MARGIN + (availableHeight - drawHeight) / 2f + drawHeight)
} else {
val drawWidth = availableHeight * imageAspect
RectF(MARGIN + (availableWidth - drawWidth) / 2f, MARGIN, MARGIN + (availableWidth - drawWidth) / 2f + drawWidth, MARGIN + availableHeight)
}
val src = Rect(0, 0, diagram.width, diagram.height)
w.canvas.drawBitmap(diagram, src, dest, Paint().apply { isFilterBitmap = true })
}
}

View File

@@ -13,11 +13,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.BatteryFull
import androidx.compose.material.icons.filled.Bolt as BoltFilled
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.BatteryFull
import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Dashboard
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.IosShare
import androidx.compose.material.icons.outlined.PictureAsPdf
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -39,6 +41,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import app.voltplan.cable.CableApplication
import app.voltplan.cable.R
import app.voltplan.cable.library.ComponentLibraryType
import app.voltplan.cable.ui.LocalUnitSettings
import app.voltplan.cable.ui.batteries.BatteriesTab
import app.voltplan.cable.ui.chargers.ChargersTab
@@ -48,7 +51,10 @@ import app.voltplan.cable.ui.overview.OverviewTab
import app.voltplan.cable.ui.sfSymbol
import app.voltplan.cable.ui.systemIconOptions
import app.voltplan.cable.ui.theme.componentColor
import app.voltplan.cable.pdf.SystemDiagram
import app.voltplan.cable.pdf.SystemOverviewPdf
import android.widget.Toast
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -82,7 +88,7 @@ fun SystemDetailScreen(
onEditCharger: (String) -> Unit,
onNewCharger: () -> Unit,
onOpenBom: () -> Unit,
onOpenLibrary: () -> Unit,
onOpenLibrary: (ComponentLibraryType) -> Unit,
) {
val context = LocalContext.current
val app = context.applicationContext as CableApplication
@@ -98,8 +104,15 @@ fun SystemDetailScreen(
var tab by rememberSaveableTab()
var showSystemEditor by remember { mutableStateOf(false) }
var showOverviewMenu by remember { mutableStateOf(false) }
var exporting by remember { mutableStateOf(false) }
val system = state.system
// Switch to the matching tab before opening an editor, so returning from the
// editor lands on that tab with the newly created component visible.
val newLoad = { tab = ComponentTab.COMPONENTS; onNewLoad() }
val newBattery = { tab = ComponentTab.BATTERIES; onNewBattery() }
val newCharger = { tab = ComponentTab.CHARGERS; onNewCharger() }
Scaffold(
topBar = {
TopAppBar(
@@ -132,28 +145,52 @@ fun SystemDetailScreen(
actions = {
when (tab) {
ComponentTab.OVERVIEW -> {
if (exporting) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp).padding(end = 12.dp),
strokeWidth = 2.dp,
)
} else {
IconButton(onClick = { showOverviewMenu = true }) {
Icon(Icons.Outlined.PictureAsPdf, contentDescription = stringResource(R.string.overview_share_pdf))
Icon(Icons.Outlined.IosShare, contentDescription = stringResource(R.string.overview_share_pdf))
}
}
DropdownMenu(expanded = showOverviewMenu, onDismissRequest = { showOverviewMenu = false }) {
DropdownMenuItem(
leadingIcon = { Icon(Icons.Outlined.Bolt, contentDescription = null) },
text = { Text(stringResource(R.string.overview_share_diagram)) },
onClick = {
showOverviewMenu = false
scope.launch {
exporting = true
SystemDiagram.exportAndShare(context, state, unitSystem) {
Toast.makeText(context, R.string.overview_share_diagram_error, Toast.LENGTH_LONG).show()
}
exporting = false
}
},
)
DropdownMenuItem(
leadingIcon = { Icon(Icons.Outlined.PictureAsPdf, contentDescription = null) },
text = { Text(stringResource(R.string.overview_share_pdf)) },
onClick = {
showOverviewMenu = false
scope.launch {
exporting = true
SystemOverviewPdf.exportAndShare(context, state, unitSystem)
exporting = false
}
},
)
}
}
ComponentTab.COMPONENTS -> IconButton(onClick = onNewLoad) {
ComponentTab.COMPONENTS -> IconButton(onClick = newLoad) {
Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.action_add))
}
ComponentTab.BATTERIES -> IconButton(onClick = onNewBattery) {
ComponentTab.BATTERIES -> IconButton(onClick = newBattery) {
Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.action_add))
}
ComponentTab.CHARGERS -> IconButton(onClick = onNewCharger) {
ComponentTab.CHARGERS -> IconButton(onClick = newCharger) {
Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.action_add))
}
}
@@ -162,10 +199,10 @@ fun SystemDetailScreen(
},
bottomBar = {
NavigationBar {
NavTab(tab, ComponentTab.OVERVIEW, Icons.Outlined.Dashboard, stringResource(R.string.tab_overview)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.COMPONENTS, Icons.Outlined.Layers, stringResource(R.string.tab_components)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.BATTERIES, Icons.Outlined.BatteryFull, stringResource(R.string.tab_batteries)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.CHARGERS, Icons.Outlined.Bolt, stringResource(R.string.tab_chargers)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.OVERVIEW, Icons.Filled.Dashboard, stringResource(R.string.tab_overview)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.COMPONENTS, Icons.Filled.Layers, stringResource(R.string.tab_components)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.BATTERIES, Icons.Filled.BatteryFull, stringResource(R.string.tab_batteries)) { tab = it; vm.logTabChange(it.analytics) }
NavTab(tab, ComponentTab.CHARGERS, Icons.Filled.BoltFilled, stringResource(R.string.tab_chargers)) { tab = it; vm.logTabChange(it.analytics) }
}
},
) { padding ->
@@ -174,11 +211,14 @@ fun SystemDetailScreen(
ComponentTab.OVERVIEW -> OverviewTab(
state = state,
unitSystem = unitSystem,
onAddLoad = onNewLoad,
onAddBattery = onNewBattery,
onAddCharger = onNewCharger,
onOpenLibrary = onOpenLibrary,
onAddLoad = newLoad,
onAddBattery = newBattery,
onAddCharger = newCharger,
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.LOAD) },
onOpenBom = { vm.logBomOpened(); onOpenBom() },
onSelectLoads = { tab = ComponentTab.COMPONENTS; vm.logTabChange(ComponentTab.COMPONENTS.analytics) },
onSelectBatteries = { tab = ComponentTab.BATTERIES; vm.logTabChange(ComponentTab.BATTERIES.analytics) },
onSelectChargers = { tab = ComponentTab.CHARGERS; vm.logTabChange(ComponentTab.CHARGERS.analytics) },
onSetRuntimeGoal = vm::setRuntimeGoal,
onSetChargeGoal = vm::setChargeGoal,
)
@@ -186,20 +226,22 @@ fun SystemDetailScreen(
state = state,
unitSystem = unitSystem,
onOpenLoad = onOpenLoad,
onNewLoad = onNewLoad,
onOpenLibrary = onOpenLibrary,
onNewLoad = newLoad,
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.LOAD) },
onDeleteLoad = vm::deleteLoad,
)
ComponentTab.BATTERIES -> BatteriesTab(
state = state,
onEditBattery = onEditBattery,
onNewBattery = onNewBattery,
onNewBattery = newBattery,
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.BATTERY) },
onDeleteBattery = vm::deleteBattery,
)
ComponentTab.CHARGERS -> ChargersTab(
state = state,
onEditCharger = onEditCharger,
onNewCharger = onNewCharger,
onNewCharger = newCharger,
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.CHARGER) },
onDeleteCharger = vm::deleteCharger,
)
}
@@ -248,4 +290,4 @@ private fun RowScope.NavTab(
}
@Composable
private fun rememberSaveableTab() = remember { mutableStateOf(ComponentTab.OVERVIEW) }
private fun rememberSaveableTab() = rememberSaveable { mutableStateOf(ComponentTab.OVERVIEW) }

View File

@@ -5,6 +5,7 @@
<!-- Actions -->
<string name="action_add">Hinzufügen</string>
<string name="action_back">Zurück</string>
<string name="action_save">Speichern</string>
<string name="action_delete">Löschen</string>
<!-- Systems -->
@@ -177,6 +178,8 @@
<string name="overview_chargers_empty_subtitle">Füge Landstrom-, DC-DC- oder Solarladegeräte hinzu, um deine Ladeleistung zu verstehen.</string>
<string name="overview_chargers_empty_create">Ladegerät hinzufügen</string>
<string name="overview_share_pdf">Vollständiger Bericht (PDF)</string>
<string name="overview_share_diagram">Schaltplan</string>
<string name="overview_share_diagram_error">Schaltplan konnte nicht erstellt werden. Überprüfe deine Internetverbindung.</string>
<!-- Goal editor steppers -->
<string name="goal_days">Tage</string>

View File

@@ -5,6 +5,7 @@
<!-- Actions -->
<string name="action_add">Añadir</string>
<string name="action_back">Atrás</string>
<string name="action_save">Guardar</string>
<string name="action_delete">Eliminar</string>
<!-- Systems -->
@@ -177,6 +178,8 @@
<string name="overview_chargers_empty_subtitle">Añade cargadores de toma de puerto, DC-DC o solares para conocer tu capacidad de carga.</string>
<string name="overview_chargers_empty_create">Añadir cargador</string>
<string name="overview_share_pdf">Informe completo (PDF)</string>
<string name="overview_share_diagram">Diagrama de cableado</string>
<string name="overview_share_diagram_error">No se pudo generar el diagrama. Comprueba tu conexión a Internet.</string>
<!-- Goal editor steppers -->
<string name="goal_days">Días</string>

View File

@@ -5,6 +5,7 @@
<!-- Actions -->
<string name="action_add">Ajouter</string>
<string name="action_back">Retour</string>
<string name="action_save">Enregistrer</string>
<string name="action_delete">Supprimer</string>
<!-- Systems -->
@@ -177,6 +178,8 @@
<string name="overview_chargers_empty_subtitle">Ajoutez des chargeurs secteur, DC-DC ou solaires pour comprendre votre capacité de charge.</string>
<string name="overview_chargers_empty_create">Ajouter un chargeur</string>
<string name="overview_share_pdf">Rapport complet (PDF)</string>
<string name="overview_share_diagram">Schéma de câblage</string>
<string name="overview_share_diagram_error">Impossible de générer le schéma. Vérifiez votre connexion Internet.</string>
<!-- Goal editor steppers -->
<string name="goal_days">Jours</string>

View File

@@ -5,6 +5,7 @@
<!-- Actions -->
<string name="action_add">Toevoegen</string>
<string name="action_back">Terug</string>
<string name="action_save">Opslaan</string>
<string name="action_delete">Verwijderen</string>
<!-- Systems -->
@@ -177,6 +178,8 @@
<string name="overview_chargers_empty_subtitle">Voeg walstroom-, DC-DC- of zonneladers toe om je laadcapaciteit te begrijpen.</string>
<string name="overview_chargers_empty_create">Lader toevoegen</string>
<string name="overview_share_pdf">Volledig rapport (PDF)</string>
<string name="overview_share_diagram">Bedradingsschema</string>
<string name="overview_share_diagram_error">Diagram kon niet worden gegenereerd. Controleer je internetverbinding.</string>
<!-- Goal editor steppers -->
<string name="goal_days">Dagen</string>

View File

@@ -5,6 +5,7 @@
<!-- Actions -->
<string name="action_add">Add</string>
<string name="action_back">Back</string>
<string name="action_save">Save</string>
<string name="action_delete">Delete</string>
<!-- Systems -->
@@ -177,6 +178,8 @@
<string name="overview_chargers_empty_subtitle">Add shore power, DC-DC, or solar chargers to understand your charging capacity.</string>
<string name="overview_chargers_empty_create">Add Charger</string>
<string name="overview_share_pdf">Full Report (PDF)</string>
<string name="overview_share_diagram">Wiring Diagram</string>
<string name="overview_share_diagram_error">Could not generate diagram. Check your internet connection.</string>
<!-- Goal editor steppers -->
<string name="goal_days">Days</string>