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.generator" = "Generator";
"charger.source.alternator" = "Alternator"; "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 // MARK: - Share Menu
"overview.share.diagram" = "Wiring Diagram"; "overview.share.diagram" = "Wiring Diagram";
"overview.share.pdf" = "Full Report (PDF)"; "overview.share.pdf" = "Full Report (PDF)";

View File

@@ -18,6 +18,7 @@ struct SystemsView: View {
@State private var showingComponentLibrary = false @State private var showingComponentLibrary = false
@State private var showingSettings = false @State private var showingSettings = false
@State private var hasPerformedInitialAutoNavigation = false @State private var hasPerformedInitialAutoNavigation = false
@State private var systemsPendingDeletion: [ElectricalSystem]?
private let systemColorOptions = [ private let systemColorOptions = [
"blue", "green", "orange", "red", "purple", "yellow", "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")) .accessibilityHint(Text("systems.list.row.accessibility.hint", comment: "Accessibility hint for systems list row"))
.accessibilityAddTraits(.isButton) .accessibilityAddTraits(.isButton)
} }
.onDelete(perform: deleteSystems) .onDelete { offsets in
systemsPendingDeletion = offsets.map { systems[$0] }
}
} }
.accessibilityIdentifier("systems-list") .accessibilityIdentifier("systems-list")
} }
@@ -133,6 +136,28 @@ struct SystemsView: View {
SettingsView() SettingsView()
.environmentObject(unitSettings) .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 { private var systemsEmptyState: some View {
@@ -416,8 +441,7 @@ struct SystemsView: View {
return candidate return candidate
} }
private func deleteSystems(offsets: IndexSet) { private func deleteSystems(_ systemsToDelete: [ElectricalSystem]) {
let systemsToDelete = offsets.map { systems[$0] }
withAnimation { withAnimation {
for system in systemsToDelete { for system in systemsToDelete {
AnalyticsTracker.log( AnalyticsTracker.log(

View File

@@ -444,6 +444,11 @@
"charger.source.generator" = "Generator"; "charger.source.generator" = "Generator";
"charger.source.alternator" = "Lichtmaschine"; "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 // MARK: - Share Menu
"overview.share.diagram" = "Schaltplan"; "overview.share.diagram" = "Schaltplan";
"overview.share.pdf" = "Vollständiger Bericht (PDF)"; "overview.share.pdf" = "Vollständiger Bericht (PDF)";

View File

@@ -406,6 +406,11 @@
"charger.source.generator" = "Generador"; "charger.source.generator" = "Generador";
"charger.source.alternator" = "Alternador"; "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 // MARK: - Share Menu
"overview.share.diagram" = "Diagrama de cableado"; "overview.share.diagram" = "Diagrama de cableado";
"overview.share.pdf" = "Informe completo (PDF)"; "overview.share.pdf" = "Informe completo (PDF)";

View File

@@ -406,6 +406,11 @@
"charger.source.generator" = "Groupe électrogène"; "charger.source.generator" = "Groupe électrogène";
"charger.source.alternator" = "Alternateur"; "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 // MARK: - Share Menu
"overview.share.diagram" = "Schéma de câblage"; "overview.share.diagram" = "Schéma de câblage";
"overview.share.pdf" = "Rapport complet (PDF)"; "overview.share.pdf" = "Rapport complet (PDF)";

View File

@@ -406,6 +406,11 @@
"charger.source.generator" = "Generator"; "charger.source.generator" = "Generator";
"charger.source.alternator" = "Dynamo"; "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 // MARK: - Share Menu
"overview.share.diagram" = "Bedradingsschema"; "overview.share.diagram" = "Bedradingsschema";
"overview.share.pdf" = "Volledig rapport (PDF)"; "overview.share.pdf" = "Volledig rapport (PDF)";

View File

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

View File

@@ -10,3 +10,5 @@
# Retrofit # Retrofit
-keep,allowobfuscation,allowshrinking interface retrofit2.Call -keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response -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.BitmapFactory
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color 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.R
import app.voltplan.cable.analytics.Analytics import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.model.effectivePowerWatts 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.model.sourceType
import app.voltplan.cable.data.UnitSystem import app.voltplan.cable.data.UnitSystem
import app.voltplan.cable.ui.system.DetailState import app.voltplan.cable.ui.system.DetailState
import app.voltplan.cable.util.Fmt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -25,13 +30,12 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit 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 { object SystemDiagram {
private const val ENDPOINT = "https://voltplan.app/api/diagram/generate" private const val ENDPOINT = "https://voltplan.app/api/diagram/generate"
private val JSON_MEDIA = "application/json; charset=utf-8".toMediaType() private val JSON_MEDIA = "application/json; charset=utf-8".toMediaType()
@@ -39,14 +43,29 @@ object SystemDiagram {
.callTimeout(15, TimeUnit.SECONDS) .callTimeout(15, TimeUnit.SECONDS)
.build() .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) { suspend fun fetch(state: DetailState, unit: UnitSystem): Bitmap? = withContext(Dispatchers.IO) {
val payload = buildPayload(state, unit) val payload = buildPayload(state, unit)
val body = Json.encodeToString(JsonObject.serializer(), payload).toRequestBody(JSON_MEDIA)
val request = Request.Builder() val request = Request.Builder()
.url(ENDPOINT) .url(ENDPOINT)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json")
.addHeader("Accept", "image/png") .addHeader("Accept", "image/png")
.post(Json.encodeToString(JsonObject.serializer(), payload).toRequestBody(JSON_MEDIA)) .post(body)
.build() .build()
runCatching { runCatching {
@@ -57,18 +76,17 @@ object SystemDiagram {
}.getOrNull() }.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( suspend fun exportAndShare(
context: Context, context: Context,
state: DetailState, state: DetailState,
unit: UnitSystem, unit: UnitSystem,
onError: () -> Unit, onError: () -> Unit,
) { ) {
val bitmap = fetch(state, unit) val bitmap = fetchOrFallback(context, state, unit)
if (bitmap == null) {
withContext(Dispatchers.Main) { onError() }
return
}
val file = withContext(Dispatchers.IO) { val file = withContext(Dispatchers.IO) {
val opaque = flattenOnWhite(bitmap) val opaque = flattenOnWhite(bitmap)
val name = state.system?.name?.takeIf { it.isNotBlank() } ?: "System" 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 { private fun flattenOnWhite(source: Bitmap): Bitmap {
val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888) val result = Bitmap.createBitmap(source.width, source.height, Bitmap.Config.ARGB_8888)
Canvas(result).apply { 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. */ /** Renders a full system overview PDF and opens the Android share sheet. */
object SystemOverviewPdf { object SystemOverviewPdf {
suspend fun exportAndShare(context: Context, state: DetailState, unit: UnitSystem) { suspend fun exportAndShare(context: Context, state: DetailState, unit: UnitSystem) {
// Fetch the wiring diagram first (falls back to no diagram page if unavailable). // Fetch the wiring diagram; falls back to a local block diagram if the API is unreachable.
val diagram = SystemDiagram.fetch(state, unit) val diagram = SystemDiagram.fetchOrFallback(context, state, unit)
val file = withContext(Dispatchers.IO) { val file = withContext(Dispatchers.IO) {
val doc = PdfDocument() val doc = PdfDocument()
val w = PdfWriter(doc) 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_batterycapacity), "${Fmt.number(m.totalUsableEnergy)} Wh")
summaryLine(w, context.getString(R.string.overview_pdf_summary_chargerpower), "${Fmt.number(m.totalChargerPower)} W") 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. // Full-page wiring diagram (always present — API result or local fallback).
diagram?.let { drawDiagramPage(w, it); w.beginPage() } drawDiagramPage(w, diagram); w.beginPage()
if (state.loads.isNotEmpty()) { if (state.loads.isNotEmpty()) {
w.gap(12f); w.text(context.getString(R.string.overview_pdf_loads_section), 18f, ACCENT, bold = true); w.divider() 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.ChevronRight
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -28,6 +29,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -59,6 +61,28 @@ fun SystemsScreen(
vm: SystemsViewModel = viewModel(), vm: SystemsViewModel = viewModel(),
) { ) {
val summaries by vm.systems.collectAsStateWithLifecycle() 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( Scaffold(
topBar = { topBar = {
@@ -99,7 +123,7 @@ fun SystemsScreen(
vm.logOpen(summary, "list") vm.logOpen(summary, "list")
onOpenSystem(summary.system.id) onOpenSystem(summary.system.id)
}, },
onDelete = { vm.deleteSystem(summary) }, onDelete = { systemPendingDeletion = summary },
) )
} }
} }

View File

@@ -76,6 +76,8 @@
<!-- Batteries --> <!-- Batteries -->
<string name="battery_bank_header_title">Batterien</string> <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_count">Batterien</string>
<string name="battery_metric_capacity">Kapazität</string> <string name="battery_metric_capacity">Kapazität</string>
<string name="battery_metric_usable_capacity">Nutzbare 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_battery_new">Neue Batterie</string>
<string name="default_charger_new">Neues Ladegerät</string> <string name="default_charger_new">Neues Ladegerät</string>
<string name="default_system_new">Neues System</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> </resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries --> <!-- Batteries -->
<string name="battery_bank_header_title">Banco de baterías</string> <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_count">Baterías</string>
<string name="battery_metric_capacity">Capacidad</string> <string name="battery_metric_capacity">Capacidad</string>
<string name="battery_metric_usable_capacity">Capacidad utilizable</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_battery_new">Nueva batería</string>
<string name="default_charger_new">Nuevo cargador</string> <string name="default_charger_new">Nuevo cargador</string>
<string name="default_system_new">Sistema nuevo</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> </resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries --> <!-- Batteries -->
<string name="battery_bank_header_title">Banque de batteries</string> <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_count">Batteries</string>
<string name="battery_metric_capacity">Capacité</string> <string name="battery_metric_capacity">Capacité</string>
<string name="battery_metric_usable_capacity">Capacité utilisable</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_battery_new">Nouvelle batterie</string>
<string name="default_charger_new">Nouveau chargeur</string> <string name="default_charger_new">Nouveau chargeur</string>
<string name="default_system_new">Nouveau système</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> </resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries --> <!-- Batteries -->
<string name="battery_bank_header_title">Accubank</string> <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_count">Batterijen</string>
<string name="battery_metric_capacity">Capaciteit</string> <string name="battery_metric_capacity">Capaciteit</string>
<string name="battery_metric_usable_capacity">Beschikbare 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_battery_new">Nieuwe batterij</string>
<string name="default_charger_new">Nieuwe lader</string> <string name="default_charger_new">Nieuwe lader</string>
<string name="default_system_new">Nieuw systeem</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> </resources>

View File

@@ -76,6 +76,8 @@
<!-- Batteries --> <!-- Batteries -->
<string name="battery_bank_header_title">Battery Bank</string> <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_count">Batteries</string>
<string name="battery_metric_capacity">Capacity</string> <string name="battery_metric_capacity">Capacity</string>
<string name="battery_metric_usable_capacity">Usable 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_battery_new">New Battery</string>
<string name="default_charger_new">New Charger</string> <string name="default_charger_new">New Charger</string>
<string name="default_system_new">New System</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> </resources>