diff --git a/Cable/Base.lproj/Localizable.strings b/Cable/Base.lproj/Localizable.strings index 540cbb4..6d36169 100644 --- a/Cable/Base.lproj/Localizable.strings +++ b/Cable/Base.lproj/Localizable.strings @@ -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)"; diff --git a/Cable/Systems/SystemsView.swift b/Cable/Systems/SystemsView.swift index e1532bd..348e301 100644 --- a/Cable/Systems/SystemsView.swift +++ b/Cable/Systems/SystemsView.swift @@ -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( diff --git a/Cable/de.lproj/Localizable.strings b/Cable/de.lproj/Localizable.strings index f96040e..ef7d9b2 100644 --- a/Cable/de.lproj/Localizable.strings +++ b/Cable/de.lproj/Localizable.strings @@ -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)"; diff --git a/Cable/es.lproj/Localizable.strings b/Cable/es.lproj/Localizable.strings index 929b243..ec1d0fb 100644 --- a/Cable/es.lproj/Localizable.strings +++ b/Cable/es.lproj/Localizable.strings @@ -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)"; diff --git a/Cable/fr.lproj/Localizable.strings b/Cable/fr.lproj/Localizable.strings index 9efbcb6..bc40033 100644 --- a/Cable/fr.lproj/Localizable.strings +++ b/Cable/fr.lproj/Localizable.strings @@ -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)"; diff --git a/Cable/nl.lproj/Localizable.strings b/Cable/nl.lproj/Localizable.strings index be58fef..ffde91d 100644 --- a/Cable/nl.lproj/Localizable.strings +++ b/Cable/nl.lproj/Localizable.strings @@ -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)"; diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d0d7fbe..50174c1 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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\"") diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index c3723a7..acb0c64 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -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 diff --git a/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt b/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt index 3b90b28..52c538b 100644 --- a/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt +++ b/android/app/src/main/java/app/voltplan/cable/pdf/SystemDiagram.kt @@ -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() + 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() + 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 { diff --git a/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt b/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt index 990ce96..5c94a6b 100644 --- a/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt +++ b/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt @@ -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() diff --git a/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt b/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt index 0374e8c..348cd57 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt @@ -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(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 }, ) } } diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index 0004d5b..7c143a5 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -76,6 +76,8 @@ Batterien + Keine Batterien + Nutzbar Batterien Kapazität Nutzbare Kapazität @@ -275,4 +277,6 @@ Neue Batterie Neues Ladegerät Neues System + System löschen? + Das System und alle seine Komponenten werden dauerhaft gelöscht. diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index 9d73322..5379d96 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -76,6 +76,8 @@ Banco de baterías + Sin baterías + Utilizable Baterías Capacidad Capacidad utilizable @@ -275,4 +277,6 @@ Nueva batería Nuevo cargador Sistema nuevo + ¿Eliminar sistema? + Esto eliminará permanentemente el sistema y todos sus componentes. diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index 222381c..c2381de 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -76,6 +76,8 @@ Banque de batteries + Pas de batteries + Utilisable Batteries Capacité Capacité utilisable @@ -275,4 +277,6 @@ Nouvelle batterie Nouveau chargeur Nouveau système + Supprimer le système ? + Cela supprimera définitivement le système et tous ses composants. diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml index 9eaa7d5..961ac6f 100644 --- a/android/app/src/main/res/values-nl/strings.xml +++ b/android/app/src/main/res/values-nl/strings.xml @@ -76,6 +76,8 @@ Accubank + Geen batterijen + Bruikbaar Batterijen Capaciteit Beschikbare capaciteit @@ -275,4 +277,6 @@ Nieuwe batterij Nieuwe lader Nieuw systeem + Systeem verwijderen? + Dit verwijdert het systeem en alle bijbehorende componenten permanent. diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 9de018c..5583917 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -76,6 +76,8 @@ Battery Bank + No batteries + Usable Batteries Capacity Usable Capacity @@ -275,4 +277,6 @@ New Battery New Charger New System + Delete System? + This will permanently delete the system and all its components.