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 showingSystemEditor = false
|
||||||
@State private var hasPresentedSystemEditorOnAppear = false
|
@State private var hasPresentedSystemEditorOnAppear = false
|
||||||
@State private var hasOpenedLoadOnAppear = false
|
@State private var hasOpenedLoadOnAppear = false
|
||||||
@State private var showingComponentLibrary = false
|
@State private var activeLibrary: ComponentLibraryType?
|
||||||
@State private var showingSystemBOM = false
|
@State private var showingSystemBOM = false
|
||||||
@State private var selectedComponentTab: ComponentTab
|
@State private var selectedComponentTab: ComponentTab
|
||||||
@State private var batteryDraft: BatteryConfiguration?
|
@State private var batteryDraft: BatteryConfiguration?
|
||||||
@@ -86,23 +86,7 @@ struct LoadsView: View {
|
|||||||
.accessibilityIdentifier("components-tab")
|
.accessibilityIdentifier("components-tab")
|
||||||
}
|
}
|
||||||
|
|
||||||
Group {
|
batteriesTab
|
||||||
if savedBatteries.isEmpty {
|
|
||||||
OnboardingInfoView(
|
|
||||||
configuration: .battery(),
|
|
||||||
onPrimaryAction: { startBatteryConfiguration() }
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
BatteriesView(
|
|
||||||
system: system,
|
|
||||||
batteries: savedBatteries,
|
|
||||||
editMode: $editMode,
|
|
||||||
onEdit: { editBattery($0) },
|
|
||||||
onDelete: deleteBatteries
|
|
||||||
)
|
|
||||||
.environment(\.editMode, $editMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tag(ComponentTab.batteries)
|
.tag(ComponentTab.batteries)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(
|
Label(
|
||||||
@@ -117,14 +101,7 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
.environment(\.editMode, $editMode)
|
.environment(\.editMode, $editMode)
|
||||||
|
|
||||||
ChargersView(
|
chargersTab
|
||||||
system: system,
|
|
||||||
chargers: savedChargers,
|
|
||||||
editMode: $editMode,
|
|
||||||
onAdd: { startChargerConfiguration() },
|
|
||||||
onEdit: { editCharger($0) },
|
|
||||||
onDelete: deleteChargers
|
|
||||||
)
|
|
||||||
.tag(ComponentTab.chargers)
|
.tag(ComponentTab.chargers)
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label(
|
Label(
|
||||||
@@ -272,9 +249,9 @@ struct LoadsView: View {
|
|||||||
exportDiagramImage()
|
exportDiagramImage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingComponentLibrary) {
|
.sheet(item: $activeLibrary) { type in
|
||||||
ComponentLibraryView { item in
|
ComponentLibraryView(libraryType: type) { item in
|
||||||
addComponent(item)
|
handleLibrarySelection(item, for: type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingSystemBOM) {
|
.sheet(isPresented: $showingSystemBOM) {
|
||||||
@@ -439,9 +416,9 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var libraryButton: some View {
|
private func libraryButton(type: ComponentLibraryType) -> some View {
|
||||||
Button {
|
Button {
|
||||||
openComponentLibrary(source: "library-button")
|
openComponentLibrary(source: "library-button", type: type)
|
||||||
} label: {
|
} label: {
|
||||||
Group {
|
Group {
|
||||||
if #available(iOS 26.0, *) {
|
if #available(iOS 26.0, *) {
|
||||||
@@ -490,6 +467,53 @@ struct LoadsView: View {
|
|||||||
.background(Color(.systemGroupedBackground))
|
.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
|
@ViewBuilder
|
||||||
private var loadsListWithHeader: some View {
|
private var loadsListWithHeader: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -507,7 +531,7 @@ struct LoadsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay(alignment: .bottomTrailing) {
|
.overlay(alignment: .bottomTrailing) {
|
||||||
libraryButton
|
libraryButton(type: .load)
|
||||||
.padding(.trailing, 24)
|
.padding(.trailing, 24)
|
||||||
.padding(.bottom, 24)
|
.padding(.bottom, 24)
|
||||||
}
|
}
|
||||||
@@ -802,15 +826,63 @@ struct LoadsView: View {
|
|||||||
showingSystemEditor = true
|
showingSystemEditor = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openComponentLibrary(source: String) {
|
private func openComponentLibrary(source: String, type: ComponentLibraryType = .load) {
|
||||||
AnalyticsTracker.log(
|
AnalyticsTracker.log(
|
||||||
"Component Library Opened",
|
"Component Library Opened",
|
||||||
properties: [
|
properties: [
|
||||||
"source": source,
|
"source": source,
|
||||||
|
"type": type.rawValue,
|
||||||
"system": system.name
|
"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() {
|
private func openBillOfMaterials() {
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ object PdfShare {
|
|||||||
return file
|
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 uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
type = "application/pdf"
|
type = mimeType
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
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
|
package app.voltplan.cable.pdf
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.RectF
|
||||||
import android.graphics.pdf.PdfDocument
|
import android.graphics.pdf.PdfDocument
|
||||||
import app.voltplan.cable.R
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.calc.ElectricalCalculations
|
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. */
|
/** 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).
|
||||||
|
val diagram = SystemDiagram.fetch(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)
|
||||||
@@ -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_batterycapacity), "${Fmt.number(m.totalCapacity)} Ah")
|
||||||
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.
|
||||||
|
diagram?.let { drawDiagramPage(w, it); 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()
|
||||||
state.loads.forEach { load ->
|
state.loads.forEach { load ->
|
||||||
@@ -89,4 +98,23 @@ object SystemOverviewPdf {
|
|||||||
private fun summaryLine(w: PdfWriter, label: String, value: String) {
|
private fun summaryLine(w: PdfWriter, label: String, value: String) {
|
||||||
w.text("$label: $value", 12f, Color.DKGRAY)
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
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.Add
|
||||||
import androidx.compose.material.icons.outlined.BatteryFull
|
|
||||||
import androidx.compose.material.icons.outlined.Bolt
|
import androidx.compose.material.icons.outlined.Bolt
|
||||||
import androidx.compose.material.icons.outlined.Dashboard
|
import androidx.compose.material.icons.outlined.IosShare
|
||||||
import androidx.compose.material.icons.outlined.Layers
|
|
||||||
import androidx.compose.material.icons.outlined.PictureAsPdf
|
import androidx.compose.material.icons.outlined.PictureAsPdf
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -39,6 +41,7 @@ import androidx.lifecycle.viewmodel.initializer
|
|||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import app.voltplan.cable.CableApplication
|
import app.voltplan.cable.CableApplication
|
||||||
import app.voltplan.cable.R
|
import app.voltplan.cable.R
|
||||||
|
import app.voltplan.cable.library.ComponentLibraryType
|
||||||
import app.voltplan.cable.ui.LocalUnitSettings
|
import app.voltplan.cable.ui.LocalUnitSettings
|
||||||
import app.voltplan.cable.ui.batteries.BatteriesTab
|
import app.voltplan.cable.ui.batteries.BatteriesTab
|
||||||
import app.voltplan.cable.ui.chargers.ChargersTab
|
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.sfSymbol
|
||||||
import app.voltplan.cable.ui.systemIconOptions
|
import app.voltplan.cable.ui.systemIconOptions
|
||||||
import app.voltplan.cable.ui.theme.componentColor
|
import app.voltplan.cable.ui.theme.componentColor
|
||||||
|
import app.voltplan.cable.pdf.SystemDiagram
|
||||||
import app.voltplan.cable.pdf.SystemOverviewPdf
|
import app.voltplan.cable.pdf.SystemOverviewPdf
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -82,7 +88,7 @@ fun SystemDetailScreen(
|
|||||||
onEditCharger: (String) -> Unit,
|
onEditCharger: (String) -> Unit,
|
||||||
onNewCharger: () -> Unit,
|
onNewCharger: () -> Unit,
|
||||||
onOpenBom: () -> Unit,
|
onOpenBom: () -> Unit,
|
||||||
onOpenLibrary: () -> Unit,
|
onOpenLibrary: (ComponentLibraryType) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val app = context.applicationContext as CableApplication
|
val app = context.applicationContext as CableApplication
|
||||||
@@ -98,8 +104,15 @@ fun SystemDetailScreen(
|
|||||||
var tab by rememberSaveableTab()
|
var tab by rememberSaveableTab()
|
||||||
var showSystemEditor by remember { mutableStateOf(false) }
|
var showSystemEditor by remember { mutableStateOf(false) }
|
||||||
var showOverviewMenu by remember { mutableStateOf(false) }
|
var showOverviewMenu by remember { mutableStateOf(false) }
|
||||||
|
var exporting by remember { mutableStateOf(false) }
|
||||||
val system = state.system
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -132,28 +145,52 @@ fun SystemDetailScreen(
|
|||||||
actions = {
|
actions = {
|
||||||
when (tab) {
|
when (tab) {
|
||||||
ComponentTab.OVERVIEW -> {
|
ComponentTab.OVERVIEW -> {
|
||||||
IconButton(onClick = { showOverviewMenu = true }) {
|
if (exporting) {
|
||||||
Icon(Icons.Outlined.PictureAsPdf, contentDescription = stringResource(R.string.overview_share_pdf))
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(24.dp).padding(end = 12.dp),
|
||||||
|
strokeWidth = 2.dp,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
IconButton(onClick = { showOverviewMenu = true }) {
|
||||||
|
Icon(Icons.Outlined.IosShare, contentDescription = stringResource(R.string.overview_share_pdf))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DropdownMenu(expanded = showOverviewMenu, onDismissRequest = { showOverviewMenu = false }) {
|
DropdownMenu(expanded = showOverviewMenu, onDismissRequest = { showOverviewMenu = false }) {
|
||||||
DropdownMenuItem(
|
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)) },
|
text = { Text(stringResource(R.string.overview_share_pdf)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
showOverviewMenu = false
|
showOverviewMenu = false
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
exporting = true
|
||||||
SystemOverviewPdf.exportAndShare(context, state, unitSystem)
|
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))
|
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))
|
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))
|
Icon(Icons.Outlined.Add, contentDescription = stringResource(R.string.action_add))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,10 +199,10 @@ fun SystemDetailScreen(
|
|||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
NavTab(tab, ComponentTab.OVERVIEW, Icons.Outlined.Dashboard, stringResource(R.string.tab_overview)) { 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.Outlined.Layers, stringResource(R.string.tab_components)) { 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.Outlined.BatteryFull, stringResource(R.string.tab_batteries)) { 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.Outlined.Bolt, stringResource(R.string.tab_chargers)) { 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 ->
|
) { padding ->
|
||||||
@@ -174,11 +211,14 @@ fun SystemDetailScreen(
|
|||||||
ComponentTab.OVERVIEW -> OverviewTab(
|
ComponentTab.OVERVIEW -> OverviewTab(
|
||||||
state = state,
|
state = state,
|
||||||
unitSystem = unitSystem,
|
unitSystem = unitSystem,
|
||||||
onAddLoad = onNewLoad,
|
onAddLoad = newLoad,
|
||||||
onAddBattery = onNewBattery,
|
onAddBattery = newBattery,
|
||||||
onAddCharger = onNewCharger,
|
onAddCharger = newCharger,
|
||||||
onOpenLibrary = onOpenLibrary,
|
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.LOAD) },
|
||||||
onOpenBom = { vm.logBomOpened(); onOpenBom() },
|
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,
|
onSetRuntimeGoal = vm::setRuntimeGoal,
|
||||||
onSetChargeGoal = vm::setChargeGoal,
|
onSetChargeGoal = vm::setChargeGoal,
|
||||||
)
|
)
|
||||||
@@ -186,20 +226,22 @@ fun SystemDetailScreen(
|
|||||||
state = state,
|
state = state,
|
||||||
unitSystem = unitSystem,
|
unitSystem = unitSystem,
|
||||||
onOpenLoad = onOpenLoad,
|
onOpenLoad = onOpenLoad,
|
||||||
onNewLoad = onNewLoad,
|
onNewLoad = newLoad,
|
||||||
onOpenLibrary = onOpenLibrary,
|
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.LOAD) },
|
||||||
onDeleteLoad = vm::deleteLoad,
|
onDeleteLoad = vm::deleteLoad,
|
||||||
)
|
)
|
||||||
ComponentTab.BATTERIES -> BatteriesTab(
|
ComponentTab.BATTERIES -> BatteriesTab(
|
||||||
state = state,
|
state = state,
|
||||||
onEditBattery = onEditBattery,
|
onEditBattery = onEditBattery,
|
||||||
onNewBattery = onNewBattery,
|
onNewBattery = newBattery,
|
||||||
|
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.BATTERY) },
|
||||||
onDeleteBattery = vm::deleteBattery,
|
onDeleteBattery = vm::deleteBattery,
|
||||||
)
|
)
|
||||||
ComponentTab.CHARGERS -> ChargersTab(
|
ComponentTab.CHARGERS -> ChargersTab(
|
||||||
state = state,
|
state = state,
|
||||||
onEditCharger = onEditCharger,
|
onEditCharger = onEditCharger,
|
||||||
onNewCharger = onNewCharger,
|
onNewCharger = newCharger,
|
||||||
|
onOpenLibrary = { onOpenLibrary(ComponentLibraryType.CHARGER) },
|
||||||
onDeleteCharger = vm::deleteCharger,
|
onDeleteCharger = vm::deleteCharger,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -248,4 +290,4 @@ private fun RowScope.NavTab(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberSaveableTab() = remember { mutableStateOf(ComponentTab.OVERVIEW) }
|
private fun rememberSaveableTab() = rememberSaveable { mutableStateOf(ComponentTab.OVERVIEW) }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_add">Hinzufügen</string>
|
<string name="action_add">Hinzufügen</string>
|
||||||
<string name="action_back">Zurück</string>
|
<string name="action_back">Zurück</string>
|
||||||
|
<string name="action_save">Speichern</string>
|
||||||
<string name="action_delete">Löschen</string>
|
<string name="action_delete">Löschen</string>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- 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_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_chargers_empty_create">Ladegerät hinzufügen</string>
|
||||||
<string name="overview_share_pdf">Vollständiger Bericht (PDF)</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 -->
|
<!-- Goal editor steppers -->
|
||||||
<string name="goal_days">Tage</string>
|
<string name="goal_days">Tage</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_add">Añadir</string>
|
<string name="action_add">Añadir</string>
|
||||||
<string name="action_back">Atrás</string>
|
<string name="action_back">Atrás</string>
|
||||||
|
<string name="action_save">Guardar</string>
|
||||||
<string name="action_delete">Eliminar</string>
|
<string name="action_delete">Eliminar</string>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- 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_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_chargers_empty_create">Añadir cargador</string>
|
||||||
<string name="overview_share_pdf">Informe completo (PDF)</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 -->
|
<!-- Goal editor steppers -->
|
||||||
<string name="goal_days">Días</string>
|
<string name="goal_days">Días</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_add">Ajouter</string>
|
<string name="action_add">Ajouter</string>
|
||||||
<string name="action_back">Retour</string>
|
<string name="action_back">Retour</string>
|
||||||
|
<string name="action_save">Enregistrer</string>
|
||||||
<string name="action_delete">Supprimer</string>
|
<string name="action_delete">Supprimer</string>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- 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_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_chargers_empty_create">Ajouter un chargeur</string>
|
||||||
<string name="overview_share_pdf">Rapport complet (PDF)</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 -->
|
<!-- Goal editor steppers -->
|
||||||
<string name="goal_days">Jours</string>
|
<string name="goal_days">Jours</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_add">Toevoegen</string>
|
<string name="action_add">Toevoegen</string>
|
||||||
<string name="action_back">Terug</string>
|
<string name="action_back">Terug</string>
|
||||||
|
<string name="action_save">Opslaan</string>
|
||||||
<string name="action_delete">Verwijderen</string>
|
<string name="action_delete">Verwijderen</string>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- 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_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_chargers_empty_create">Lader toevoegen</string>
|
||||||
<string name="overview_share_pdf">Volledig rapport (PDF)</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 -->
|
<!-- Goal editor steppers -->
|
||||||
<string name="goal_days">Dagen</string>
|
<string name="goal_days">Dagen</string>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<string name="action_add">Add</string>
|
<string name="action_add">Add</string>
|
||||||
<string name="action_back">Back</string>
|
<string name="action_back">Back</string>
|
||||||
|
<string name="action_save">Save</string>
|
||||||
<string name="action_delete">Delete</string>
|
<string name="action_delete">Delete</string>
|
||||||
|
|
||||||
<!-- Systems -->
|
<!-- 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_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_chargers_empty_create">Add Charger</string>
|
||||||
<string name="overview_share_pdf">Full Report (PDF)</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 -->
|
<!-- Goal editor steppers -->
|
||||||
<string name="goal_days">Days</string>
|
<string name="goal_days">Days</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user