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:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user