Require confirmation before deleting a system (iOS + Android)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 14:46:32 +02:00
parent d4b490ea07
commit 01cdaf1861
16 changed files with 367 additions and 23 deletions

View File

@@ -379,6 +379,11 @@
"charger.source.generator" = "Generator";
"charger.source.alternator" = "Alternator";
// MARK: - System Deletion
"systems.delete.confirm.title" = "Delete System?";
"systems.delete.confirm.message" = "This will permanently delete the system and all its components.";
"systems.delete.confirm.button" = "Delete";
// MARK: - Share Menu
"overview.share.diagram" = "Wiring Diagram";
"overview.share.pdf" = "Full Report (PDF)";

View File

@@ -18,6 +18,7 @@ struct SystemsView: View {
@State private var showingComponentLibrary = false
@State private var showingSettings = false
@State private var hasPerformedInitialAutoNavigation = false
@State private var systemsPendingDeletion: [ElectricalSystem]?
private let systemColorOptions = [
"blue", "green", "orange", "red", "purple", "yellow",
@@ -87,7 +88,9 @@ struct SystemsView: View {
.accessibilityHint(Text("systems.list.row.accessibility.hint", comment: "Accessibility hint for systems list row"))
.accessibilityAddTraits(.isButton)
}
.onDelete(perform: deleteSystems)
.onDelete { offsets in
systemsPendingDeletion = offsets.map { systems[$0] }
}
}
.accessibilityIdentifier("systems-list")
}
@@ -133,6 +136,28 @@ struct SystemsView: View {
SettingsView()
.environmentObject(unitSettings)
}
.alert(
String(localized: "systems.delete.confirm.title", defaultValue: "Delete System?"),
isPresented: Binding(
get: { systemsPendingDeletion != nil },
set: { if !$0 { systemsPendingDeletion = nil } }
)
) {
Button(
String(localized: "systems.delete.confirm.button", defaultValue: "Delete"),
role: .destructive
) {
if let pending = systemsPendingDeletion {
deleteSystems(pending)
systemsPendingDeletion = nil
}
}
Button(String(localized: "battery.editor.alert.cancel", defaultValue: "Cancel"), role: .cancel) {
systemsPendingDeletion = nil
}
} message: {
Text(String(localized: "systems.delete.confirm.message", defaultValue: "This will permanently delete the system and all its components."))
}
}
private var systemsEmptyState: some View {
@@ -416,8 +441,7 @@ struct SystemsView: View {
return candidate
}
private func deleteSystems(offsets: IndexSet) {
let systemsToDelete = offsets.map { systems[$0] }
private func deleteSystems(_ systemsToDelete: [ElectricalSystem]) {
withAnimation {
for system in systemsToDelete {
AnalyticsTracker.log(

View File

@@ -444,6 +444,11 @@
"charger.source.generator" = "Generator";
"charger.source.alternator" = "Lichtmaschine";
// MARK: - System Deletion
"systems.delete.confirm.title" = "System löschen?";
"systems.delete.confirm.message" = "Das System und alle seine Komponenten werden dauerhaft gelöscht.";
"systems.delete.confirm.button" = "Löschen";
// MARK: - Share Menu
"overview.share.diagram" = "Schaltplan";
"overview.share.pdf" = "Vollständiger Bericht (PDF)";

View File

@@ -406,6 +406,11 @@
"charger.source.generator" = "Generador";
"charger.source.alternator" = "Alternador";
// MARK: - System Deletion
"systems.delete.confirm.title" = "¿Eliminar sistema?";
"systems.delete.confirm.message" = "Esto eliminará permanentemente el sistema y todos sus componentes.";
"systems.delete.confirm.button" = "Eliminar";
// MARK: - Share Menu
"overview.share.diagram" = "Diagrama de cableado";
"overview.share.pdf" = "Informe completo (PDF)";

View File

@@ -406,6 +406,11 @@
"charger.source.generator" = "Groupe électrogène";
"charger.source.alternator" = "Alternateur";
// MARK: - System Deletion
"systems.delete.confirm.title" = "Supprimer le système ?";
"systems.delete.confirm.message" = "Cela supprimera définitivement le système et tous ses composants.";
"systems.delete.confirm.button" = "Supprimer";
// MARK: - Share Menu
"overview.share.diagram" = "Schéma de câblage";
"overview.share.pdf" = "Rapport complet (PDF)";

View File

@@ -406,6 +406,11 @@
"charger.source.generator" = "Generator";
"charger.source.alternator" = "Dynamo";
// MARK: - System Deletion
"systems.delete.confirm.title" = "Systeem verwijderen?";
"systems.delete.confirm.message" = "Dit verwijdert het systeem en alle bijbehorende componenten permanent.";
"systems.delete.confirm.button" = "Verwijderen";
// MARK: - Share Menu
"overview.share.diagram" = "Bedradingsschema";
"overview.share.pdf" = "Volledig rapport (PDF)";

View File

@@ -25,8 +25,8 @@ android {
applicationId = "app.voltplan.cable"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
versionCode = 85
versionName = "1.7.0"
// Aptabase analytics — mirrors the iOS configuration (the iPhone app's tracker).
buildConfigField("String", "APTABASE_APP_KEY", "\"A-SH-4260269603\"")

View File

@@ -10,3 +10,5 @@
# Retrofit
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
# Play Review KTX
-dontwarn com.google.android.gms.common.annotation.NoNullnessRewrite

View File

@@ -5,6 +5,10 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Typeface
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.model.effectivePowerWatts
@@ -12,6 +16,7 @@ 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 app.voltplan.cable.util.Fmt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
@@ -25,13 +30,12 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
/**
* 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()
@@ -39,14 +43,29 @@ object SystemDiagram {
.callTimeout(15, TimeUnit.SECONDS)
.build()
/** Fetches the diagram as a [Bitmap], or null on any network/decoding failure. */
// Fallback bitmap dimensions and layout (mirrors iOS drawSystemDiagram)
private const val FB_W = 1200
private const val FB_H = 750
private const val FB_MARGIN = 48f
private const val FB_BLOCK_H = 100f
private const val FB_BLOCK_SPACING = 14f
private const val FB_BLOCK_INSET = 24f
private const val FB_MAX_BLOCKS = 4
private const val FB_BATT_MIN_H = 260f
private val FB_CHARGER_COLOR = Color.rgb(51, 140, 222)
private val FB_BATTERY_COLOR = Color.rgb(77, 176, 79)
private val FB_LOAD_COLOR = Color.rgb(237, 140, 36)
private val FB_CARD_BG = Color.rgb(245, 245, 247)
/** Fetches the wiring diagram PNG from the VoltPlan API; returns null on any failure. */
suspend fun fetch(state: DetailState, unit: UnitSystem): Bitmap? = withContext(Dispatchers.IO) {
val payload = buildPayload(state, unit)
val body = Json.encodeToString(JsonObject.serializer(), payload).toRequestBody(JSON_MEDIA)
val request = Request.Builder()
.url(ENDPOINT)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "image/png")
.post(Json.encodeToString(JsonObject.serializer(), payload).toRequestBody(JSON_MEDIA))
.post(body)
.build()
runCatching {
@@ -57,18 +76,17 @@ object SystemDiagram {
}.getOrNull()
}
/** Fetches the diagram, flattens it onto a white background, and opens the Android share sheet. */
/** Always returns a diagram — API result when reachable, local block diagram otherwise. */
suspend fun fetchOrFallback(context: Context, state: DetailState, unit: UnitSystem): Bitmap =
fetch(state, unit) ?: drawFallback(context, state)
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 bitmap = fetchOrFallback(context, state, unit)
val file = withContext(Dispatchers.IO) {
val opaque = flattenOnWhite(bitmap)
val name = state.system?.name?.takeIf { it.isNotBlank() } ?: "System"
@@ -83,6 +101,237 @@ object SystemDiagram {
}
}
// -------------------------------------------------------------------------
// Offline fallback: 3-column block diagram (mirrors iOS drawSystemDiagram)
// -------------------------------------------------------------------------
private fun drawFallback(context: Context, state: DetailState): Bitmap {
val bmp = Bitmap.createBitmap(FB_W, FB_H, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bmp)
canvas.drawColor(Color.WHITE)
val colW = (FB_W - FB_MARGIN * 2) / 3f
val chargerX = FB_MARGIN
val batteryX = FB_MARGIN + colW
val loadX = FB_MARGIN + colW * 2f
// Card background
val cardPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = FB_CARD_BG
style = Paint.Style.FILL
}
canvas.drawRoundRect(
RectF(FB_MARGIN, FB_MARGIN, FB_W - FB_MARGIN, FB_H - FB_MARGIN),
24f, 24f, cardPaint,
)
// Column headers
val headerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GRAY
textSize = 32f
typeface = Typeface.DEFAULT_BOLD
textAlign = Paint.Align.CENTER
}
val headerY = FB_MARGIN + 44f
canvas.drawText(
context.getString(R.string.overview_pdf_chargers_section).uppercase(Locale.getDefault()),
chargerX + colW / 2f, headerY, headerPaint,
)
canvas.drawText(
context.getString(R.string.battery_bank_header_title).uppercase(Locale.getDefault()),
batteryX + colW / 2f, headerY, headerPaint,
)
canvas.drawText(
context.getString(R.string.overview_pdf_loads_section).uppercase(Locale.getDefault()),
loadX + colW / 2f, headerY, headerPaint,
)
val contentTop = headerY + 14f
// Charger blocks
val chargersToShow = state.chargers.take(FB_MAX_BLOCKS)
val chargerCentersY = mutableListOf<Float>()
chargersToShow.forEachIndexed { i, charger ->
val y = contentTop + i * (FB_BLOCK_H + FB_BLOCK_SPACING)
val rect = RectF(chargerX + FB_BLOCK_INSET, y, chargerX + colW - FB_BLOCK_INSET, y + FB_BLOCK_H)
drawComponentBlock(canvas, charger.name, "${Fmt.number(charger.effectivePowerWatts)} W", FB_CHARGER_COLOR, rect)
chargerCentersY.add(rect.centerY())
}
if (state.chargers.isEmpty()) drawDash(canvas, chargerX + colW / 2f, contentTop + 34f)
else if (state.chargers.size > FB_MAX_BLOCKS) drawMore(canvas, "+${state.chargers.size - FB_MAX_BLOCKS}", chargerX + colW / 2f, contentTop + FB_MAX_BLOCKS * (FB_BLOCK_H + FB_BLOCK_SPACING) + 26f)
// Battery block (height matches the taller outer column, minimum 260px)
val outerRows = maxOf(chargersToShow.size, minOf(state.loads.size, FB_MAX_BLOCKS)).coerceAtLeast(1)
val battH = (outerRows * (FB_BLOCK_H + FB_BLOCK_SPACING) - FB_BLOCK_SPACING).coerceAtLeast(FB_BATT_MIN_H)
val battRect = RectF(
batteryX + FB_BLOCK_INSET, contentTop,
batteryX + colW - FB_BLOCK_INSET, contentTop + battH,
)
drawBatteryBlock(context, canvas, state, battRect)
val batteryCenterY = battRect.centerY()
// Load blocks
val loadsToShow = state.loads.take(FB_MAX_BLOCKS)
val loadCentersY = mutableListOf<Float>()
loadsToShow.forEachIndexed { i, load ->
val y = contentTop + i * (FB_BLOCK_H + FB_BLOCK_SPACING)
val rect = RectF(loadX + FB_BLOCK_INSET, y, loadX + colW - FB_BLOCK_INSET, y + FB_BLOCK_H)
drawComponentBlock(canvas, load.name, "${Fmt.number(load.power)} W", FB_LOAD_COLOR, rect)
loadCentersY.add(rect.centerY())
}
if (state.loads.isEmpty()) drawDash(canvas, loadX + colW / 2f, contentTop + 34f)
else if (state.loads.size > FB_MAX_BLOCKS) drawMore(canvas, "+${state.loads.size - FB_MAX_BLOCKS}", loadX + colW / 2f, contentTop + FB_MAX_BLOCKS * (FB_BLOCK_H + FB_BLOCK_SPACING) + 26f)
// Arrows: chargers → battery, battery → loads
val chargerRight = chargerX + colW - FB_BLOCK_INSET
val battLeft = battRect.left
val battRight = battRect.right
val loadLeft = loadX + FB_BLOCK_INSET
for (cy in chargerCentersY) drawArrow(canvas, chargerRight, cy, battLeft, batteryCenterY, FB_CHARGER_COLOR)
for (cy in loadCentersY) drawArrow(canvas, battRight, batteryCenterY, loadLeft, cy, FB_LOAD_COLOR)
return bmp
}
/** White card with colored left accent bar, name (bold), and a detail line. */
private fun drawComponentBlock(canvas: Canvas, name: String, detail: String, color: Int, rect: RectF) {
val p = Paint(Paint.ANTI_ALIAS_FLAG)
p.color = Color.WHITE
p.style = Paint.Style.FILL
canvas.drawRoundRect(rect, 10f, 10f, p)
p.color = Color.argb(55, 0, 0, 0)
p.style = Paint.Style.STROKE
p.strokeWidth = 1.5f
canvas.drawRoundRect(rect, 10f, 10f, p)
// Left accent bar (rounded on left, square on right so it sits flush)
p.color = color
p.style = Paint.Style.FILL
canvas.drawRoundRect(RectF(rect.left, rect.top, rect.left + 7f, rect.bottom), 10f, 10f, p)
canvas.drawRect(RectF(rect.left + 4f, rect.top, rect.left + 7f, rect.bottom), p)
val textLeft = rect.left + 16f
p.color = Color.BLACK
p.textSize = 28f
p.typeface = Typeface.DEFAULT_BOLD
p.textAlign = Paint.Align.LEFT
p.style = Paint.Style.FILL
val maxW = rect.width() - 20f
val nameChars = p.breakText(name, true, maxW - p.measureText("…"), null)
val nameStr = if (nameChars < name.length) name.take(nameChars) + "…" else name
canvas.drawText(nameStr, textLeft, rect.top + 36f, p)
p.color = Color.DKGRAY
p.textSize = 22f
p.typeface = Typeface.DEFAULT
canvas.drawText(detail, textLeft, rect.top + 66f, p)
}
/** Green-tinted block showing battery bank totals (capacity, energy, usable). */
private fun drawBatteryBlock(context: Context, canvas: Canvas, state: DetailState, rect: RectF) {
val p = Paint(Paint.ANTI_ALIAS_FLAG)
val r = Color.red(FB_BATTERY_COLOR); val g = Color.green(FB_BATTERY_COLOR); val b = Color.blue(FB_BATTERY_COLOR)
p.color = Color.argb(20, r, g, b)
p.style = Paint.Style.FILL
canvas.drawRoundRect(rect, 14f, 14f, p)
p.color = Color.argb(100, r, g, b)
p.style = Paint.Style.STROKE
p.strokeWidth = 2.5f
canvas.drawRoundRect(rect, 14f, 14f, p)
p.style = Paint.Style.FILL
p.textAlign = Paint.Align.CENTER
val cx = rect.centerX()
if (state.batteries.isEmpty()) {
p.color = Color.GRAY
p.textSize = 24f
p.typeface = Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
canvas.drawText(context.getString(R.string.diagram_battery_no_batteries), cx, rect.centerY() + 10f, p)
return
}
val m = state.metrics
p.color = FB_BATTERY_COLOR
p.textSize = 48f
p.typeface = Typeface.DEFAULT
canvas.drawText("⚡", cx, rect.top + 60f, p)
p.color = Color.BLACK
p.textSize = 42f
p.typeface = Typeface.DEFAULT_BOLD
canvas.drawText("${Fmt.number(m.totalCapacity)} Ah", cx, rect.top + 112f, p)
p.color = Color.DKGRAY
p.textSize = 32f
p.typeface = Typeface.DEFAULT
canvas.drawText("${Fmt.number(m.totalEnergy)} Wh", cx, rect.top + 152f, p)
p.color = Color.GRAY
p.textSize = 26f
canvas.drawText("${context.getString(R.string.diagram_battery_usable)}: ${Fmt.number(m.totalUsableCapacity)} Ah", cx, rect.top + 186f, p)
if (state.batteries.size > 1) {
p.color = FB_BATTERY_COLOR
p.textSize = 26f
canvas.drawText("${state.batteries.size}×", cx, rect.bottom - 22f, p)
}
}
/** Cubic bezier arrow from (x1, y1) to (x2, y2) with a filled triangular head. */
private fun drawArrow(canvas: Canvas, x1: Float, y1: Float, x2: Float, y2: Float, color: Int) {
val cr = Color.red(color); val cg = Color.green(color); val cb = Color.blue(color)
val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.color = Color.argb(128, cr, cg, cb)
style = Paint.Style.STROKE
strokeWidth = 2.5f
strokeCap = Paint.Cap.ROUND
}
val ctrlX = (x1 + x2) / 2f
val curvePath = Path().apply {
moveTo(x1, y1)
cubicTo(ctrlX, y1, ctrlX, y2, x2, y2)
}
canvas.drawPath(curvePath, linePaint)
val angle = atan2((y2 - y1).toDouble(), (x2 - x1).toDouble())
val s = 10.0
val headPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.color = Color.argb(128, cr, cg, cb)
style = Paint.Style.FILL
}
val headPath = Path().apply {
moveTo(x2, y2)
lineTo((x2 - s * cos(angle - Math.PI / 6)).toFloat(), (y2 - s * sin(angle - Math.PI / 6)).toFloat())
lineTo((x2 - s * cos(angle + Math.PI / 6)).toFloat(), (y2 - s * sin(angle + Math.PI / 6)).toFloat())
close()
}
canvas.drawPath(headPath, headPaint)
}
private fun drawDash(canvas: Canvas, cx: Float, y: Float) {
val p = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GRAY; textSize = 28f; textAlign = Paint.Align.CENTER
}
canvas.drawText("–", cx, y, p)
}
private fun drawMore(canvas: Canvas, text: String, cx: Float, y: Float) {
val p = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.GRAY; textSize = 26f; textAlign = Paint.Align.CENTER
}
canvas.drawText(text, cx, y, p)
}
// -------------------------------------------------------------------------
private fun flattenOnWhite(source: Bitmap): Bitmap {
val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
Canvas(result).apply {

View File

@@ -28,8 +28,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)
// Fetch the wiring diagram; falls back to a local block diagram if the API is unreachable.
val diagram = SystemDiagram.fetchOrFallback(context, state, unit)
val file = withContext(Dispatchers.IO) {
val doc = PdfDocument()
val w = PdfWriter(doc)
@@ -49,8 +49,8 @@ object SystemOverviewPdf {
summaryLine(w, context.getString(R.string.overview_pdf_summary_batterycapacity), "${Fmt.number(m.totalUsableEnergy)} Wh")
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() }
// Full-page wiring diagram (always present — API result or local fallback).
drawDiagramPage(w, diagram); 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()

View File

@@ -19,6 +19,7 @@ import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.ChevronRight
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -28,6 +29,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -59,6 +61,28 @@ fun SystemsScreen(
vm: SystemsViewModel = viewModel(),
) {
val summaries by vm.systems.collectAsStateWithLifecycle()
var systemPendingDeletion by remember { mutableStateOf<SystemSummary?>(null) }
systemPendingDeletion?.let { pending ->
AlertDialog(
onDismissRequest = { systemPendingDeletion = null },
title = { Text(stringResource(R.string.delete_system_confirm_title)) },
text = { Text(stringResource(R.string.delete_system_confirm_message)) },
confirmButton = {
TextButton(onClick = {
vm.deleteSystem(pending)
systemPendingDeletion = null
}) {
Text(stringResource(R.string.action_delete))
}
},
dismissButton = {
TextButton(onClick = { systemPendingDeletion = null }) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
Scaffold(
topBar = {
@@ -99,7 +123,7 @@ fun SystemsScreen(
vm.logOpen(summary, "list")
onOpenSystem(summary.system.id)
},
onDelete = { vm.deleteSystem(summary) },
onDelete = { systemPendingDeletion = summary },
)
}
}

View File

@@ -76,6 +76,8 @@
<!-- Batteries -->
<string name="battery_bank_header_title">Batterien</string>
<string name="diagram_battery_no_batteries">Keine Batterien</string>
<string name="diagram_battery_usable">Nutzbar</string>
<string name="battery_metric_count">Batterien</string>
<string name="battery_metric_capacity">Kapazität</string>
<string name="battery_metric_usable_capacity">Nutzbare Kapazität</string>
@@ -275,4 +277,6 @@
<string name="default_battery_new">Neue Batterie</string>
<string name="default_charger_new">Neues Ladegerät</string>
<string name="default_system_new">Neues System</string>
<string name="delete_system_confirm_title">System löschen?</string>
<string name="delete_system_confirm_message">Das System und alle seine Komponenten werden dauerhaft gelöscht.</string>
</resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries -->
<string name="battery_bank_header_title">Banco de baterías</string>
<string name="diagram_battery_no_batteries">Sin baterías</string>
<string name="diagram_battery_usable">Utilizable</string>
<string name="battery_metric_count">Baterías</string>
<string name="battery_metric_capacity">Capacidad</string>
<string name="battery_metric_usable_capacity">Capacidad utilizable</string>
@@ -275,4 +277,6 @@
<string name="default_battery_new">Nueva batería</string>
<string name="default_charger_new">Nuevo cargador</string>
<string name="default_system_new">Sistema nuevo</string>
<string name="delete_system_confirm_title">¿Eliminar sistema?</string>
<string name="delete_system_confirm_message">Esto eliminará permanentemente el sistema y todos sus componentes.</string>
</resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries -->
<string name="battery_bank_header_title">Banque de batteries</string>
<string name="diagram_battery_no_batteries">Pas de batteries</string>
<string name="diagram_battery_usable">Utilisable</string>
<string name="battery_metric_count">Batteries</string>
<string name="battery_metric_capacity">Capacité</string>
<string name="battery_metric_usable_capacity">Capacité utilisable</string>
@@ -275,4 +277,6 @@
<string name="default_battery_new">Nouvelle batterie</string>
<string name="default_charger_new">Nouveau chargeur</string>
<string name="default_system_new">Nouveau système</string>
<string name="delete_system_confirm_title">Supprimer le système ?</string>
<string name="delete_system_confirm_message">Cela supprimera définitivement le système et tous ses composants.</string>
</resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries -->
<string name="battery_bank_header_title">Accubank</string>
<string name="diagram_battery_no_batteries">Geen batterijen</string>
<string name="diagram_battery_usable">Bruikbaar</string>
<string name="battery_metric_count">Batterijen</string>
<string name="battery_metric_capacity">Capaciteit</string>
<string name="battery_metric_usable_capacity">Beschikbare capaciteit</string>
@@ -275,4 +277,6 @@
<string name="default_battery_new">Nieuwe batterij</string>
<string name="default_charger_new">Nieuwe lader</string>
<string name="default_system_new">Nieuw systeem</string>
<string name="delete_system_confirm_title">Systeem verwijderen?</string>
<string name="delete_system_confirm_message">Dit verwijdert het systeem en alle bijbehorende componenten permanent.</string>
</resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries -->
<string name="battery_bank_header_title">Battery Bank</string>
<string name="diagram_battery_no_batteries">No batteries</string>
<string name="diagram_battery_usable">Usable</string>
<string name="battery_metric_count">Batteries</string>
<string name="battery_metric_capacity">Capacity</string>
<string name="battery_metric_usable_capacity">Usable Capacity</string>
@@ -275,4 +277,6 @@
<string name="default_battery_new">New Battery</string>
<string name="default_charger_new">New Charger</string>
<string name="default_system_new">New System</string>
<string name="delete_system_confirm_title">Delete System?</string>
<string name="delete_system_confirm_message">This will permanently delete the system and all its components.</string>
</resources>