Align Android with iOS reference behavior

- Overview PDF: usable energy summary, cable length, power loss,
  usable energy rows (matching iOS tables)
- Analytics: align event names and properties with iOS
- Localize default component names and editor dialog labels
- Unit system default via device measurement system (API 28+)
- Preserve BOM/affiliate fields when editing batteries/chargers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 12:36:18 +02:00
parent 89ee36c1a4
commit d4b490ea07
19 changed files with 191 additions and 43 deletions

View File

@@ -1,5 +1,8 @@
package app.voltplan.cable.data
import android.icu.util.LocaleData
import android.icu.util.ULocale
import android.os.Build
import java.util.Locale
/** Metric (mm², m) or imperial (AWG, ft). Mirrors the iOS `UnitSystem` enum. */
@@ -13,10 +16,16 @@ enum class UnitSystem(val rawValue: String) {
companion object {
fun fromRaw(raw: String?): UnitSystem? = entries.firstOrNull { it.rawValue == raw }
/** Default for the device locale — US measurement system implies imperial. */
/** Default for the device locale — mirrors iOS `Locale.measurementSystem == .us`. */
fun deviceDefault(): UnitSystem {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val system = runCatching { LocaleData.getMeasurementSystem(ULocale.getDefault()) }.getOrNull()
if (system != null) {
return if (system == LocaleData.MeasurementSystem.US) IMPERIAL else METRIC
}
}
val country = Locale.getDefault().country.uppercase()
// Countries that customarily use US/imperial measurements.
// Fallback: countries that customarily use US/imperial measurements.
return if (country in setOf("US", "LR", "MM")) IMPERIAL else METRIC
}
}

View File

@@ -3,6 +3,7 @@ package app.voltplan.cable.data.model
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
import app.voltplan.cable.data.LocaleDefaults
import java.util.UUID
/**
@@ -78,7 +79,7 @@ data class SavedBattery(
data class SavedCharger(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val name: String = "",
val inputVoltage: Double = 230.0,
val inputVoltage: Double = LocaleDefaults.mainsVoltage,
val outputVoltage: Double = 14.2,
val maxCurrentAmps: Double = 30.0,
val maxPowerWatts: Double = 0.0,

View File

@@ -3,6 +3,7 @@ package app.voltplan.cable.library
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.voltplan.cable.CableApplication
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.model.Chemistry
import app.voltplan.cable.data.model.ElectricalSystem
@@ -55,10 +56,14 @@ class ComponentLibraryViewModel(
fun refresh() = load()
fun setQuery(q: String) { _state.value = _state.value.copy(query = q) }
/** Resolves the display name of [systemId], or null when unknown. */
suspend fun systemName(systemId: String?): String? =
systemId?.let { repo.getSystem(it)?.name }
/** Returns the system to add into, creating a new one when [targetSystemId] is null. */
private suspend fun ensureSystem(targetSystemId: String?): Pair<String, Boolean> {
if (targetSystemId != null) return targetSystemId to false
val name = repo.uniqueSystemName("New System")
val name = repo.uniqueSystemName(app.getString(R.string.default_system_new))
val system = ElectricalSystem(name = name, iconName = SystemIconMapper.iconFor(name), colorName = SystemIconMapper.colorOptions.random())
repo.upsertSystem(system)
Analytics.log("System Created", mapOf("name" to name, "source" to "library"))
@@ -95,7 +100,7 @@ class ComponentLibraryViewModel(
affiliateCountryCode = affiliate?.country,
)
repo.upsertLoad(load)
Analytics.log("Library Load Added", mapOf("id" to item.id, "name" to item.localizedName, "system" to systemId))
Analytics.log("Library Load Added", mapOf("id" to item.id, "name" to item.localizedName, "system" to (systemName(systemId) ?: "")))
onDone(if (createdNewSystem) systemId else null)
}
@@ -123,7 +128,7 @@ class ComponentLibraryViewModel(
affiliateCountryCode = affiliate?.country,
)
repo.upsertBattery(battery)
Analytics.log("Library Battery Added", mapOf("id" to item.id, "name" to item.localizedName, "system" to systemId))
Analytics.log("Library Battery Added", mapOf("id" to item.id, "name" to item.localizedName, "system" to (systemName(systemId) ?: "")))
onDone(systemId, battery.id)
}
@@ -157,7 +162,7 @@ class ComponentLibraryViewModel(
powerSourceType = sourceType.rawValue,
)
repo.upsertCharger(charger)
Analytics.log("Library Charger Added", mapOf("id" to item.id, "name" to item.localizedName, "system" to systemId))
Analytics.log("Library Charger Added", mapOf("id" to item.id, "name" to item.localizedName, "system" to (systemName(systemId) ?: "")))
onDone(systemId, charger.id)
}

View File

@@ -16,6 +16,7 @@ import app.voltplan.cable.data.model.chemistry
import app.voltplan.cable.data.model.effectivePowerWatts
import app.voltplan.cable.data.model.energyWattHours
import app.voltplan.cable.data.model.usableCapacityAmpHours
import app.voltplan.cable.data.model.usableEnergyWattHours
import app.voltplan.cable.ui.system.DetailState
import app.voltplan.cable.util.Fmt
import kotlinx.coroutines.Dispatchers
@@ -45,7 +46,7 @@ object SystemOverviewPdf {
summaryLine(w, context.getString(R.string.overview_pdf_summary_chargetime), m.estimatedChargeHours?.let { formatDurationHours(it) } ?: "—")
summaryLine(w, context.getString(R.string.overview_pdf_summary_totalpower), "${Fmt.number(m.totalPower)} W")
summaryLine(w, context.getString(R.string.overview_pdf_summary_totalcurrent), "${Fmt.number(m.totalCurrent)} A")
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.totalUsableEnergy)} Wh")
summaryLine(w, context.getString(R.string.overview_pdf_summary_chargerpower), "${Fmt.number(m.totalChargerPower)} W")
// Full-page wiring diagram, followed by a fresh page for the entity tables.
@@ -58,9 +59,11 @@ object SystemOverviewPdf {
val cs = ElectricalCalculations.recommendedCrossSection(load.length, load.current, load.voltage, unit)
val gauge = if (unit == UnitSystem.METRIC) String.format(Locale.US, "%.1f mm²", cs) else "${ElectricalCalculations.formatAWG(cs)} AWG"
val vdrop = ElectricalCalculations.voltageDropPercentage(load.length, load.current, load.voltage, unit)
val ploss = ElectricalCalculations.powerLoss(load.length, load.current, load.voltage, unit)
summaryLine(w, "${context.getString(R.string.overview_pdf_load_voltage)} / ${context.getString(R.string.overview_pdf_load_current)}", String.format(Locale.US, "%.1f V / %.1f A", load.voltage, load.current))
summaryLine(w, "${context.getString(R.string.overview_pdf_load_power)} / ${context.getString(R.string.overview_pdf_load_cable)}", "${Fmt.number(load.power)} W / $gauge")
summaryLine(w, "${context.getString(R.string.overview_pdf_load_vdrop)} / ${context.getString(R.string.overview_pdf_load_fuse)}", String.format(Locale.US, "%.1f%% / %.0f A", vdrop, ElectricalCalculations.recommendedFuse(load.current)))
summaryLine(w, "${context.getString(R.string.overview_pdf_load_power)} / ${context.getString(R.string.overview_pdf_load_length)}", "${Fmt.number(load.power)} W / ${Fmt.number(load.length)} ${unit.lengthUnit}")
summaryLine(w, "${context.getString(R.string.overview_pdf_load_cable)} / ${context.getString(R.string.overview_pdf_load_vdrop)}", String.format(Locale.US, "%s / %.1f%%", gauge, vdrop))
summaryLine(w, "${context.getString(R.string.overview_pdf_load_ploss)} / ${context.getString(R.string.overview_pdf_load_fuse)}", String.format(Locale.US, "%s W / %.0f A", Fmt.number(ploss), ElectricalCalculations.recommendedFuse(load.current)))
w.divider()
}
}
@@ -71,7 +74,7 @@ object SystemOverviewPdf {
w.text(b.name, 14f, bold = true)
summaryLine(w, "${context.getString(R.string.overview_pdf_battery_chemistry)} / ${context.getString(R.string.overview_pdf_battery_voltage)}", "${b.chemistry.displayName} / ${Fmt.number(b.nominalVoltage)} V")
summaryLine(w, "${context.getString(R.string.overview_pdf_battery_capacity)} / ${context.getString(R.string.overview_pdf_battery_usable)}", "${Fmt.number(b.capacityAmpHours)} Ah / ${Fmt.number(b.usableCapacityAmpHours)} Ah")
summaryLine(w, context.getString(R.string.overview_pdf_battery_energy), "${Fmt.number(b.energyWattHours)} Wh")
summaryLine(w, "${context.getString(R.string.overview_pdf_battery_energy)} / ${context.getString(R.string.overview_pdf_battery_usableenergy)}", "${Fmt.number(b.energyWattHours)} Wh / ${Fmt.number(b.usableEnergyWattHours)} Wh")
w.divider()
}
}

View File

@@ -3,6 +3,7 @@ package app.voltplan.cable.ui.batteries
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.voltplan.cable.CableApplication
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.model.Chemistry
import app.voltplan.cable.data.model.SavedBattery
@@ -14,7 +15,7 @@ import java.util.UUID
import kotlin.math.abs
data class BatteryState(
val name: String = "New Battery",
val name: String = "",
val chemistry: Chemistry = Chemistry.LIFEPO4,
val nominalVoltage: Double = 12.8,
val capacityAmpHours: Double = 100.0,
@@ -33,7 +34,7 @@ data class BatteryState(
}
class BatteryEditorViewModel(
app: CableApplication,
private val app: CableApplication,
private val systemId: String,
batteryId: String?,
) : ViewModel() {
@@ -41,14 +42,25 @@ class BatteryEditorViewModel(
private var id: String = batteryId ?: UUID.randomUUID().toString()
private val isNew = batteryId == null
private var loggedCreate = false
private var systemName: String = ""
// Fields the editor doesn't touch but must survive a save round-trip.
private var affiliateURLString: String? = null
private var affiliateCountryCode: String? = null
private var bomCompletedItemIDs: List<String> = emptyList()
private val _state = MutableStateFlow(BatteryState())
val state: StateFlow<BatteryState> = _state.asStateFlow()
init {
viewModelScope.launch {
val system = repo.getSystem(systemId)
systemName = system?.name ?: ""
if (batteryId != null) {
repo.getBattery(batteryId)?.let { b ->
affiliateURLString = b.affiliateURLString
affiliateCountryCode = b.affiliateCountryCode
bomCompletedItemIDs = b.bomCompletedItemIDs
_state.value = BatteryState(
name = b.name,
chemistry = Chemistry.fromRaw(b.chemistryRawValue),
@@ -64,14 +76,22 @@ class BatteryEditorViewModel(
)
}
} else {
val color = repo.getSystem(systemId)?.colorName ?: "blue"
val name = repo.uniqueComponentName(systemId, "New Battery")
val color = system?.colorName ?: "blue"
val name = repo.uniqueComponentName(systemId, app.getString(R.string.default_battery_new))
_state.value = _state.value.copy(name = name, colorName = color)
}
Analytics.log("Battery Editor Opened", mapOf("source" to if (isNew) "create" else "edit"))
Analytics.log("Battery Editor Opened", mapOf("source" to if (isNew) "create" else "edit", "system" to systemName))
}
}
override fun onCleared() {
// Mirrors iOS, which logs "Battery Updated" when the editor for an existing battery closes.
if (!isNew) {
Analytics.log("Battery Updated", mapOf("name" to _state.value.name, "system" to systemName))
}
super.onCleared()
}
private fun update(transform: (BatteryState) -> BatteryState) {
_state.value = transform(_state.value)
persist()
@@ -120,13 +140,16 @@ class BatteryEditorViewModel(
iconName = s.iconName,
colorName = s.colorName,
systemId = systemId,
affiliateURLString = affiliateURLString,
affiliateCountryCode = affiliateCountryCode,
bomCompletedItemIDs = bomCompletedItemIDs,
timestamp = System.currentTimeMillis(),
)
viewModelScope.launch {
repo.upsertBattery(battery)
if (isNew && !loggedCreate) {
loggedCreate = true
Analytics.log("Battery Created", mapOf("name" to s.name, "voltage" to s.nominalVoltage, "capacity" to s.capacityAmpHours, "chemistry" to s.chemistry.rawValue))
Analytics.log("Battery Created", mapOf("name" to s.name, "system" to systemName))
}
}
}

View File

@@ -3,6 +3,7 @@ package app.voltplan.cable.ui.chargers
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.voltplan.cable.CableApplication
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.LocaleDefaults
import app.voltplan.cable.data.model.PowerSourceType
@@ -17,7 +18,7 @@ import java.util.UUID
enum class PowerEntryMode { CURRENT, POWER }
data class ChargerState(
val name: String = "New Charger",
val name: String = "",
val sourceType: PowerSourceType = PowerSourceType.SHORE,
val inputVoltage: Double = 230.0,
val outputVoltage: Double = 14.2,
@@ -38,7 +39,7 @@ data class ChargerState(
}
class ChargerEditorViewModel(
app: CableApplication,
private val app: CableApplication,
private val systemId: String,
chargerId: String?,
) : ViewModel() {
@@ -46,14 +47,27 @@ class ChargerEditorViewModel(
private var id: String = chargerId ?: UUID.randomUUID().toString()
private val isNew = chargerId == null
private var loggedCreate = false
private var systemName: String = ""
// Fields the editor doesn't touch but must survive a save round-trip.
private var remoteIconURLString: String? = null
private var affiliateURLString: String? = null
private var affiliateCountryCode: String? = null
private var bomCompletedItemIDs: List<String> = emptyList()
private val _state = MutableStateFlow(ChargerState(inputVoltage = LocaleDefaults.mainsVoltage))
val state: StateFlow<ChargerState> = _state.asStateFlow()
init {
viewModelScope.launch {
val system = repo.getSystem(systemId)
systemName = system?.name ?: ""
if (chargerId != null) {
repo.getCharger(chargerId)?.let { c ->
remoteIconURLString = c.remoteIconURLString
affiliateURLString = c.affiliateURLString
affiliateCountryCode = c.affiliateCountryCode
bomCompletedItemIDs = c.bomCompletedItemIDs
val mode = if (c.maxPowerWatts > 0) PowerEntryMode.POWER else PowerEntryMode.CURRENT
val current = if (c.maxPowerWatts > 0 && c.outputVoltage > 0) Fmt.roundToTenth(c.maxPowerWatts / c.outputVoltage) else c.maxCurrentAmps
_state.value = ChargerState(
@@ -70,14 +84,22 @@ class ChargerEditorViewModel(
)
}
} else {
val color = repo.getSystem(systemId)?.colorName ?: "orange"
val name = repo.uniqueComponentName(systemId, "New Charger")
val color = system?.colorName ?: "orange"
val name = repo.uniqueComponentName(systemId, app.getString(R.string.default_charger_new))
_state.value = _state.value.copy(name = name, colorName = color)
}
Analytics.log("Charger Editor Opened", mapOf("source" to if (isNew) "create" else "edit"))
Analytics.log("Charger Editor Opened", mapOf("source" to if (isNew) "create" else "edit", "system" to systemName))
}
}
override fun onCleared() {
// Mirrors iOS, which logs "Charger Updated" when the editor for an existing charger closes.
if (!isNew) {
Analytics.log("Charger Updated", mapOf("name" to _state.value.name, "system" to systemName))
}
super.onCleared()
}
private fun update(transform: (ChargerState) -> ChargerState) {
_state.value = transform(_state.value)
persist()
@@ -135,13 +157,17 @@ class ChargerEditorViewModel(
colorName = s.colorName,
systemId = systemId,
timestamp = System.currentTimeMillis(),
remoteIconURLString = remoteIconURLString,
affiliateURLString = affiliateURLString,
affiliateCountryCode = affiliateCountryCode,
bomCompletedItemIDs = bomCompletedItemIDs,
powerSourceType = s.sourceType.rawValue,
)
viewModelScope.launch {
repo.upsertCharger(charger)
if (isNew && !loggedCreate) {
loggedCreate = true
Analytics.log("Charger Created", mapOf("name" to s.name, "output_voltage" to s.outputVoltage, "max_current" to s.maxCurrentAmps, "max_power" to s.maxPowerWatts))
Analytics.log("Charger Created", mapOf("name" to s.name, "system" to systemName))
}
}
}

View File

@@ -37,6 +37,8 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.res.stringResource
import app.voltplan.cable.R
import app.voltplan.cable.ui.sfSymbol
import app.voltplan.cable.ui.theme.componentColor
import app.voltplan.cable.ui.theme.curatedColorNames
@@ -77,7 +79,7 @@ fun AppearanceEditorSheet(
Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
androidx.compose.foundation.layout.Spacer(Modifier.weight(1f))
TextButton(onClick = { onSave(name, icon, color); onDismiss() }) {
Text("Save", fontWeight = FontWeight.SemiBold)
Text(stringResource(R.string.action_save), fontWeight = FontWeight.SemiBold)
}
}
@@ -112,7 +114,7 @@ fun AppearanceEditorSheet(
extra?.invoke()
Text("Icon", style = MaterialTheme.typography.titleSmall)
Text(stringResource(R.string.editor_icon), style = MaterialTheme.typography.titleSmall)
GridRows(items = icons, columns = 5) { symbol ->
val selected = symbol == icon
Box(
@@ -133,7 +135,7 @@ fun AppearanceEditorSheet(
}
}
Text("Color", style = MaterialTheme.typography.titleSmall)
Text(stringResource(R.string.editor_color), style = MaterialTheme.typography.titleSmall)
GridRows(items = curatedColorNames, columns = 6) { colorName ->
val c = componentColor(colorName)
Box(

View File

@@ -20,9 +20,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import app.voltplan.cable.R
import app.voltplan.cable.util.Fmt
import kotlin.math.abs
@@ -113,8 +115,8 @@ fun ValueEditDialog(
}
},
confirmButton = {
TextButton(onClick = { Fmt.parseInput(text)?.let(onConfirm); onDismiss() }) { Text("Save") }
TextButton(onClick = { Fmt.parseInput(text)?.let(onConfirm); onDismiss() }) { Text(stringResource(R.string.action_save)) }
},
dismissButton = { TextButton(onClick = onDismiss) { Text("Cancel") } },
dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(R.string.action_cancel)) } },
)
}

View File

@@ -67,13 +67,12 @@ fun ComponentLibraryScreen(
val state by vm.state.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
Analytics.log(
"Component Library Opened",
mapOf(
val props = mutableMapOf<String, Any>(
"source" to if (targetSystemId != null) "system" else "systems-list",
"type" to libraryType.typeValue,
),
)
vm.systemName(targetSystemId)?.let { props["system"] = it }
Analytics.log("Component Library Opened", props)
}
Scaffold(

View File

@@ -3,6 +3,7 @@ package app.voltplan.cable.ui.loads
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.voltplan.cable.CableApplication
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.calc.ElectricalCalculations
import app.voltplan.cable.data.UnitSystem
@@ -15,7 +16,7 @@ import kotlinx.coroutines.launch
import java.util.UUID
data class CalcState(
val loadName: String = "My Load",
val loadName: String = "",
val voltage: Double = 12.0,
val current: Double = 5.0,
val power: Double = 60.0,
@@ -36,7 +37,7 @@ data class CalcState(
}
class CalculatorViewModel(
app: CableApplication,
private val app: CableApplication,
private val systemId: String,
loadId: String?,
) : ViewModel() {
@@ -73,11 +74,15 @@ class CalculatorViewModel(
}
}
} else {
// Mirror SystemComponentsPersistence default load values.
_state.value = CalcState()
// Mirror SystemComponentsPersistence default load values, including the
// localized unique default name ("New Load", "New Load 2", ...).
viewModelScope.launch {
val name = repo.uniqueComponentName(systemId, app.getString(R.string.default_load_new))
_state.value = CalcState(loadName = name)
persist(created = true)
}
}
}
private fun mode() = if (_state.value.isWattMode) "watt" else "amp"
private suspend fun systemName() = repo.getSystem(systemId)?.name ?: ""

View File

@@ -138,7 +138,10 @@ fun SystemDetailScreen(
system.name,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp).clickable { showSystemEditor = true },
modifier = Modifier.padding(start = 8.dp).clickable {
vm.logSystemEditorOpened("toolbar")
showSystemEditor = true
},
)
}
}

View File

@@ -43,11 +43,14 @@ class SystemDetailViewModel(
Analytics.log("Tab Changed", mapOf("tab" to tab, "system" to (state.value.system?.name ?: "")))
}
fun logSystemEditorOpened(source: String) {
Analytics.log("System Editor Opened", mapOf("source" to source, "system" to (state.value.system?.name ?: "")))
}
fun saveSystem(name: String, location: String, iconName: String, colorName: String) {
val current = state.value.system ?: return
viewModelScope.launch {
repo.upsertSystem(current.copy(name = name, location = location, iconName = iconName, colorName = colorName))
Analytics.log("System Updated", mapOf("name" to name))
}
}

View File

@@ -84,7 +84,7 @@ fun SystemsScreen(
SystemsOnboarding(
modifier = Modifier.padding(padding),
onCreate = { name ->
vm.createSystem(name, source = "onboarding", randomColor = true, onCreated = onOpenSystem)
vm.createSystem(name, source = "named", randomColor = true, onCreated = onOpenSystem)
},
)
} else {
@@ -167,6 +167,11 @@ private fun SystemsOnboarding(modifier: Modifier = Modifier, onCreate: (String)
val defaultName = stringResource(R.string.default_system_name)
val effective = remember(name) { name }
// Mirrors iOS, which logs "Launched" when the onboarding view appears.
androidx.compose.runtime.LaunchedEffect(Unit) {
app.voltplan.cable.analytics.Analytics.log("Launched")
}
Column(
modifier = modifier
.fillMaxSize()

View File

@@ -4,6 +4,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import app.voltplan.cable.CableApplication
import app.voltplan.cable.R
import app.voltplan.cable.analytics.Analytics
import app.voltplan.cable.data.model.ElectricalSystem
import app.voltplan.cable.data.model.SavedLoad
@@ -37,7 +38,8 @@ class SystemsViewModel(app: Application) : AndroidViewModel(app) {
/** Creates a system and returns its id via the callback. [source] is for analytics. */
fun createSystem(preferredName: String?, source: String, randomColor: Boolean, onCreated: (String) -> Unit) {
viewModelScope.launch {
val base = preferredName?.trim().takeUnless { it.isNullOrEmpty() } ?: "New System"
val base = preferredName?.trim().takeUnless { it.isNullOrEmpty() }
?: getApplication<Application>().getString(R.string.default_system_new)
val name = repo.uniqueSystemName(base)
val color = if (randomColor) SystemIconMapper.colorOptions.random() else "blue"
val system = ElectricalSystem(

View File

@@ -263,4 +263,16 @@
<!-- Misc -->
<string name="component_fallback_name">Komponente</string>
<!-- Cross-platform parity additions (mirroring iOS Localizable.strings) -->
<string name="action_cancel">Abbrechen</string>
<string name="editor_icon">Symbol</string>
<string name="editor_color">Farbe</string>
<string name="overview_pdf_load_length">Kabellänge</string>
<string name="overview_pdf_load_ploss">Leistungsverlust</string>
<string name="overview_pdf_battery_usableenergy">Nutzbare Energie</string>
<string name="default_load_new">Neuer Verbraucher</string>
<string name="default_battery_new">Neue Batterie</string>
<string name="default_charger_new">Neues Ladegerät</string>
<string name="default_system_new">Neues System</string>
</resources>

View File

@@ -263,4 +263,16 @@
<!-- Misc -->
<string name="component_fallback_name">Componente</string>
<!-- Cross-platform parity additions (mirroring iOS Localizable.strings) -->
<string name="action_cancel">Cancelar</string>
<string name="editor_icon">Icono</string>
<string name="editor_color">Color</string>
<string name="overview_pdf_load_length">Longitud del cable</string>
<string name="overview_pdf_load_ploss">Pérdida de potencia</string>
<string name="overview_pdf_battery_usableenergy">Energía utilizable</string>
<string name="default_load_new">Carga nueva</string>
<string name="default_battery_new">Nueva batería</string>
<string name="default_charger_new">Nuevo cargador</string>
<string name="default_system_new">Sistema nuevo</string>
</resources>

View File

@@ -263,4 +263,16 @@
<!-- Misc -->
<string name="component_fallback_name">Composant</string>
<!-- Cross-platform parity additions (mirroring iOS Localizable.strings) -->
<string name="action_cancel">Annuler</string>
<string name="editor_icon">Icône</string>
<string name="editor_color">Couleur</string>
<string name="overview_pdf_load_length">Longueur du câble</string>
<string name="overview_pdf_load_ploss">Perte de puissance</string>
<string name="overview_pdf_battery_usableenergy">Énergie utilisable</string>
<string name="default_load_new">Nouvelle charge</string>
<string name="default_battery_new">Nouvelle batterie</string>
<string name="default_charger_new">Nouveau chargeur</string>
<string name="default_system_new">Nouveau système</string>
</resources>

View File

@@ -263,4 +263,16 @@
<!-- Misc -->
<string name="component_fallback_name">Component</string>
<!-- Cross-platform parity additions (mirroring iOS Localizable.strings) -->
<string name="action_cancel">Annuleren</string>
<string name="editor_icon">Pictogram</string>
<string name="editor_color">Kleur</string>
<string name="overview_pdf_load_length">Kabellengte</string>
<string name="overview_pdf_load_ploss">Vermogensverlies</string>
<string name="overview_pdf_battery_usableenergy">Bruikbare energie</string>
<string name="default_load_new">Nieuwe last</string>
<string name="default_battery_new">Nieuwe batterij</string>
<string name="default_charger_new">Nieuwe lader</string>
<string name="default_system_new">Nieuw systeem</string>
</resources>

View File

@@ -263,4 +263,16 @@
<!-- Misc -->
<string name="component_fallback_name">Component</string>
<!-- Cross-platform parity additions (mirroring iOS Localizable.strings) -->
<string name="action_cancel">Cancel</string>
<string name="editor_icon">Icon</string>
<string name="editor_color">Color</string>
<string name="overview_pdf_load_length">Cable Length</string>
<string name="overview_pdf_load_ploss">Power Loss</string>
<string name="overview_pdf_battery_usableenergy">Usable Energy</string>
<string name="default_load_new">New Load</string>
<string name="default_battery_new">New Battery</string>
<string name="default_charger_new">New Charger</string>
<string name="default_system_new">New System</string>
</resources>