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:
@@ -1,5 +1,8 @@
|
|||||||
package app.voltplan.cable.data
|
package app.voltplan.cable.data
|
||||||
|
|
||||||
|
import android.icu.util.LocaleData
|
||||||
|
import android.icu.util.ULocale
|
||||||
|
import android.os.Build
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
/** Metric (mm², m) or imperial (AWG, ft). Mirrors the iOS `UnitSystem` enum. */
|
/** Metric (mm², m) or imperial (AWG, ft). Mirrors the iOS `UnitSystem` enum. */
|
||||||
@@ -13,10 +16,16 @@ enum class UnitSystem(val rawValue: String) {
|
|||||||
companion object {
|
companion object {
|
||||||
fun fromRaw(raw: String?): UnitSystem? = entries.firstOrNull { it.rawValue == raw }
|
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 {
|
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()
|
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
|
return if (country in setOf("US", "LR", "MM")) IMPERIAL else METRIC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.voltplan.cable.data.model
|
|||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import app.voltplan.cable.data.LocaleDefaults
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +79,7 @@ data class SavedBattery(
|
|||||||
data class SavedCharger(
|
data class SavedCharger(
|
||||||
@PrimaryKey val id: String = UUID.randomUUID().toString(),
|
@PrimaryKey val id: String = UUID.randomUUID().toString(),
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
val inputVoltage: Double = 230.0,
|
val inputVoltage: Double = LocaleDefaults.mainsVoltage,
|
||||||
val outputVoltage: Double = 14.2,
|
val outputVoltage: Double = 14.2,
|
||||||
val maxCurrentAmps: Double = 30.0,
|
val maxCurrentAmps: Double = 30.0,
|
||||||
val maxPowerWatts: Double = 0.0,
|
val maxPowerWatts: Double = 0.0,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.voltplan.cable.library
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.voltplan.cable.CableApplication
|
import app.voltplan.cable.CableApplication
|
||||||
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.analytics.Analytics
|
import app.voltplan.cable.analytics.Analytics
|
||||||
import app.voltplan.cable.data.model.Chemistry
|
import app.voltplan.cable.data.model.Chemistry
|
||||||
import app.voltplan.cable.data.model.ElectricalSystem
|
import app.voltplan.cable.data.model.ElectricalSystem
|
||||||
@@ -55,10 +56,14 @@ class ComponentLibraryViewModel(
|
|||||||
fun refresh() = load()
|
fun refresh() = load()
|
||||||
fun setQuery(q: String) { _state.value = _state.value.copy(query = q) }
|
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. */
|
/** Returns the system to add into, creating a new one when [targetSystemId] is null. */
|
||||||
private suspend fun ensureSystem(targetSystemId: String?): Pair<String, Boolean> {
|
private suspend fun ensureSystem(targetSystemId: String?): Pair<String, Boolean> {
|
||||||
if (targetSystemId != null) return targetSystemId to false
|
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())
|
val system = ElectricalSystem(name = name, iconName = SystemIconMapper.iconFor(name), colorName = SystemIconMapper.colorOptions.random())
|
||||||
repo.upsertSystem(system)
|
repo.upsertSystem(system)
|
||||||
Analytics.log("System Created", mapOf("name" to name, "source" to "library"))
|
Analytics.log("System Created", mapOf("name" to name, "source" to "library"))
|
||||||
@@ -95,7 +100,7 @@ class ComponentLibraryViewModel(
|
|||||||
affiliateCountryCode = affiliate?.country,
|
affiliateCountryCode = affiliate?.country,
|
||||||
)
|
)
|
||||||
repo.upsertLoad(load)
|
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)
|
onDone(if (createdNewSystem) systemId else null)
|
||||||
}
|
}
|
||||||
@@ -123,7 +128,7 @@ class ComponentLibraryViewModel(
|
|||||||
affiliateCountryCode = affiliate?.country,
|
affiliateCountryCode = affiliate?.country,
|
||||||
)
|
)
|
||||||
repo.upsertBattery(battery)
|
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)
|
onDone(systemId, battery.id)
|
||||||
}
|
}
|
||||||
@@ -157,7 +162,7 @@ class ComponentLibraryViewModel(
|
|||||||
powerSourceType = sourceType.rawValue,
|
powerSourceType = sourceType.rawValue,
|
||||||
)
|
)
|
||||||
repo.upsertCharger(charger)
|
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)
|
onDone(systemId, charger.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import app.voltplan.cable.data.model.chemistry
|
|||||||
import app.voltplan.cable.data.model.effectivePowerWatts
|
import app.voltplan.cable.data.model.effectivePowerWatts
|
||||||
import app.voltplan.cable.data.model.energyWattHours
|
import app.voltplan.cable.data.model.energyWattHours
|
||||||
import app.voltplan.cable.data.model.usableCapacityAmpHours
|
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.ui.system.DetailState
|
||||||
import app.voltplan.cable.util.Fmt
|
import app.voltplan.cable.util.Fmt
|
||||||
import kotlinx.coroutines.Dispatchers
|
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_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_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_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")
|
summaryLine(w, context.getString(R.string.overview_pdf_summary_chargerpower), "${Fmt.number(m.totalChargerPower)} W")
|
||||||
|
|
||||||
// Full-page wiring diagram, followed by a fresh page for the entity tables.
|
// Full-page wiring diagram, 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 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 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 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_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_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_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_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()
|
w.divider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +74,7 @@ object SystemOverviewPdf {
|
|||||||
w.text(b.name, 14f, bold = true)
|
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_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_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()
|
w.divider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.voltplan.cable.ui.batteries
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.voltplan.cable.CableApplication
|
import app.voltplan.cable.CableApplication
|
||||||
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.analytics.Analytics
|
import app.voltplan.cable.analytics.Analytics
|
||||||
import app.voltplan.cable.data.model.Chemistry
|
import app.voltplan.cable.data.model.Chemistry
|
||||||
import app.voltplan.cable.data.model.SavedBattery
|
import app.voltplan.cable.data.model.SavedBattery
|
||||||
@@ -14,7 +15,7 @@ import java.util.UUID
|
|||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
data class BatteryState(
|
data class BatteryState(
|
||||||
val name: String = "New Battery",
|
val name: String = "",
|
||||||
val chemistry: Chemistry = Chemistry.LIFEPO4,
|
val chemistry: Chemistry = Chemistry.LIFEPO4,
|
||||||
val nominalVoltage: Double = 12.8,
|
val nominalVoltage: Double = 12.8,
|
||||||
val capacityAmpHours: Double = 100.0,
|
val capacityAmpHours: Double = 100.0,
|
||||||
@@ -33,7 +34,7 @@ data class BatteryState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class BatteryEditorViewModel(
|
class BatteryEditorViewModel(
|
||||||
app: CableApplication,
|
private val app: CableApplication,
|
||||||
private val systemId: String,
|
private val systemId: String,
|
||||||
batteryId: String?,
|
batteryId: String?,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -41,14 +42,25 @@ class BatteryEditorViewModel(
|
|||||||
private var id: String = batteryId ?: UUID.randomUUID().toString()
|
private var id: String = batteryId ?: UUID.randomUUID().toString()
|
||||||
private val isNew = batteryId == null
|
private val isNew = batteryId == null
|
||||||
private var loggedCreate = false
|
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())
|
private val _state = MutableStateFlow(BatteryState())
|
||||||
val state: StateFlow<BatteryState> = _state.asStateFlow()
|
val state: StateFlow<BatteryState> = _state.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val system = repo.getSystem(systemId)
|
||||||
|
systemName = system?.name ?: ""
|
||||||
if (batteryId != null) {
|
if (batteryId != null) {
|
||||||
repo.getBattery(batteryId)?.let { b ->
|
repo.getBattery(batteryId)?.let { b ->
|
||||||
|
affiliateURLString = b.affiliateURLString
|
||||||
|
affiliateCountryCode = b.affiliateCountryCode
|
||||||
|
bomCompletedItemIDs = b.bomCompletedItemIDs
|
||||||
_state.value = BatteryState(
|
_state.value = BatteryState(
|
||||||
name = b.name,
|
name = b.name,
|
||||||
chemistry = Chemistry.fromRaw(b.chemistryRawValue),
|
chemistry = Chemistry.fromRaw(b.chemistryRawValue),
|
||||||
@@ -64,14 +76,22 @@ class BatteryEditorViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val color = repo.getSystem(systemId)?.colorName ?: "blue"
|
val color = system?.colorName ?: "blue"
|
||||||
val name = repo.uniqueComponentName(systemId, "New Battery")
|
val name = repo.uniqueComponentName(systemId, app.getString(R.string.default_battery_new))
|
||||||
_state.value = _state.value.copy(name = name, colorName = color)
|
_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) {
|
private fun update(transform: (BatteryState) -> BatteryState) {
|
||||||
_state.value = transform(_state.value)
|
_state.value = transform(_state.value)
|
||||||
persist()
|
persist()
|
||||||
@@ -120,13 +140,16 @@ class BatteryEditorViewModel(
|
|||||||
iconName = s.iconName,
|
iconName = s.iconName,
|
||||||
colorName = s.colorName,
|
colorName = s.colorName,
|
||||||
systemId = systemId,
|
systemId = systemId,
|
||||||
|
affiliateURLString = affiliateURLString,
|
||||||
|
affiliateCountryCode = affiliateCountryCode,
|
||||||
|
bomCompletedItemIDs = bomCompletedItemIDs,
|
||||||
timestamp = System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
)
|
)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repo.upsertBattery(battery)
|
repo.upsertBattery(battery)
|
||||||
if (isNew && !loggedCreate) {
|
if (isNew && !loggedCreate) {
|
||||||
loggedCreate = true
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.voltplan.cable.ui.chargers
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.voltplan.cable.CableApplication
|
import app.voltplan.cable.CableApplication
|
||||||
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.analytics.Analytics
|
import app.voltplan.cable.analytics.Analytics
|
||||||
import app.voltplan.cable.data.LocaleDefaults
|
import app.voltplan.cable.data.LocaleDefaults
|
||||||
import app.voltplan.cable.data.model.PowerSourceType
|
import app.voltplan.cable.data.model.PowerSourceType
|
||||||
@@ -17,7 +18,7 @@ import java.util.UUID
|
|||||||
enum class PowerEntryMode { CURRENT, POWER }
|
enum class PowerEntryMode { CURRENT, POWER }
|
||||||
|
|
||||||
data class ChargerState(
|
data class ChargerState(
|
||||||
val name: String = "New Charger",
|
val name: String = "",
|
||||||
val sourceType: PowerSourceType = PowerSourceType.SHORE,
|
val sourceType: PowerSourceType = PowerSourceType.SHORE,
|
||||||
val inputVoltage: Double = 230.0,
|
val inputVoltage: Double = 230.0,
|
||||||
val outputVoltage: Double = 14.2,
|
val outputVoltage: Double = 14.2,
|
||||||
@@ -38,7 +39,7 @@ data class ChargerState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ChargerEditorViewModel(
|
class ChargerEditorViewModel(
|
||||||
app: CableApplication,
|
private val app: CableApplication,
|
||||||
private val systemId: String,
|
private val systemId: String,
|
||||||
chargerId: String?,
|
chargerId: String?,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -46,14 +47,27 @@ class ChargerEditorViewModel(
|
|||||||
private var id: String = chargerId ?: UUID.randomUUID().toString()
|
private var id: String = chargerId ?: UUID.randomUUID().toString()
|
||||||
private val isNew = chargerId == null
|
private val isNew = chargerId == null
|
||||||
private var loggedCreate = false
|
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))
|
private val _state = MutableStateFlow(ChargerState(inputVoltage = LocaleDefaults.mainsVoltage))
|
||||||
val state: StateFlow<ChargerState> = _state.asStateFlow()
|
val state: StateFlow<ChargerState> = _state.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
val system = repo.getSystem(systemId)
|
||||||
|
systemName = system?.name ?: ""
|
||||||
if (chargerId != null) {
|
if (chargerId != null) {
|
||||||
repo.getCharger(chargerId)?.let { c ->
|
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 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
|
val current = if (c.maxPowerWatts > 0 && c.outputVoltage > 0) Fmt.roundToTenth(c.maxPowerWatts / c.outputVoltage) else c.maxCurrentAmps
|
||||||
_state.value = ChargerState(
|
_state.value = ChargerState(
|
||||||
@@ -70,14 +84,22 @@ class ChargerEditorViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val color = repo.getSystem(systemId)?.colorName ?: "orange"
|
val color = system?.colorName ?: "orange"
|
||||||
val name = repo.uniqueComponentName(systemId, "New Charger")
|
val name = repo.uniqueComponentName(systemId, app.getString(R.string.default_charger_new))
|
||||||
_state.value = _state.value.copy(name = name, colorName = color)
|
_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) {
|
private fun update(transform: (ChargerState) -> ChargerState) {
|
||||||
_state.value = transform(_state.value)
|
_state.value = transform(_state.value)
|
||||||
persist()
|
persist()
|
||||||
@@ -135,13 +157,17 @@ class ChargerEditorViewModel(
|
|||||||
colorName = s.colorName,
|
colorName = s.colorName,
|
||||||
systemId = systemId,
|
systemId = systemId,
|
||||||
timestamp = System.currentTimeMillis(),
|
timestamp = System.currentTimeMillis(),
|
||||||
|
remoteIconURLString = remoteIconURLString,
|
||||||
|
affiliateURLString = affiliateURLString,
|
||||||
|
affiliateCountryCode = affiliateCountryCode,
|
||||||
|
bomCompletedItemIDs = bomCompletedItemIDs,
|
||||||
powerSourceType = s.sourceType.rawValue,
|
powerSourceType = s.sourceType.rawValue,
|
||||||
)
|
)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repo.upsertCharger(charger)
|
repo.upsertCharger(charger)
|
||||||
if (isNew && !loggedCreate) {
|
if (isNew && !loggedCreate) {
|
||||||
loggedCreate = true
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import androidx.compose.ui.draw.clip
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.sfSymbol
|
||||||
import app.voltplan.cable.ui.theme.componentColor
|
import app.voltplan.cable.ui.theme.componentColor
|
||||||
import app.voltplan.cable.ui.theme.curatedColorNames
|
import app.voltplan.cable.ui.theme.curatedColorNames
|
||||||
@@ -77,7 +79,7 @@ fun AppearanceEditorSheet(
|
|||||||
Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
Text(title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||||
androidx.compose.foundation.layout.Spacer(Modifier.weight(1f))
|
androidx.compose.foundation.layout.Spacer(Modifier.weight(1f))
|
||||||
TextButton(onClick = { onSave(name, icon, color); onDismiss() }) {
|
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()
|
extra?.invoke()
|
||||||
|
|
||||||
Text("Icon", style = MaterialTheme.typography.titleSmall)
|
Text(stringResource(R.string.editor_icon), style = MaterialTheme.typography.titleSmall)
|
||||||
GridRows(items = icons, columns = 5) { symbol ->
|
GridRows(items = icons, columns = 5) { symbol ->
|
||||||
val selected = symbol == icon
|
val selected = symbol == icon
|
||||||
Box(
|
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 ->
|
GridRows(items = curatedColorNames, columns = 6) { colorName ->
|
||||||
val c = componentColor(colorName)
|
val c = componentColor(colorName)
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.util.Fmt
|
import app.voltplan.cable.util.Fmt
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
@@ -113,8 +115,8 @@ fun ValueEditDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
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)) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,13 +67,12 @@ fun ComponentLibraryScreen(
|
|||||||
val state by vm.state.collectAsStateWithLifecycle()
|
val state by vm.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
Analytics.log(
|
val props = mutableMapOf<String, Any>(
|
||||||
"Component Library Opened",
|
"source" to if (targetSystemId != null) "system" else "systems-list",
|
||||||
mapOf(
|
"type" to libraryType.typeValue,
|
||||||
"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(
|
Scaffold(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package app.voltplan.cable.ui.loads
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.voltplan.cable.CableApplication
|
import app.voltplan.cable.CableApplication
|
||||||
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.analytics.Analytics
|
import app.voltplan.cable.analytics.Analytics
|
||||||
import app.voltplan.cable.calc.ElectricalCalculations
|
import app.voltplan.cable.calc.ElectricalCalculations
|
||||||
import app.voltplan.cable.data.UnitSystem
|
import app.voltplan.cable.data.UnitSystem
|
||||||
@@ -15,7 +16,7 @@ import kotlinx.coroutines.launch
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class CalcState(
|
data class CalcState(
|
||||||
val loadName: String = "My Load",
|
val loadName: String = "",
|
||||||
val voltage: Double = 12.0,
|
val voltage: Double = 12.0,
|
||||||
val current: Double = 5.0,
|
val current: Double = 5.0,
|
||||||
val power: Double = 60.0,
|
val power: Double = 60.0,
|
||||||
@@ -36,7 +37,7 @@ data class CalcState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CalculatorViewModel(
|
class CalculatorViewModel(
|
||||||
app: CableApplication,
|
private val app: CableApplication,
|
||||||
private val systemId: String,
|
private val systemId: String,
|
||||||
loadId: String?,
|
loadId: String?,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@@ -73,9 +74,13 @@ class CalculatorViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mirror SystemComponentsPersistence default load values.
|
// Mirror SystemComponentsPersistence default load values, including the
|
||||||
_state.value = CalcState()
|
// localized unique default name ("New Load", "New Load 2", ...).
|
||||||
persist(created = true)
|
viewModelScope.launch {
|
||||||
|
val name = repo.uniqueComponentName(systemId, app.getString(R.string.default_load_new))
|
||||||
|
_state.value = CalcState(loadName = name)
|
||||||
|
persist(created = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,10 @@ fun SystemDetailScreen(
|
|||||||
system.name,
|
system.name,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
style = MaterialTheme.typography.titleMedium,
|
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
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,14 @@ class SystemDetailViewModel(
|
|||||||
Analytics.log("Tab Changed", mapOf("tab" to tab, "system" to (state.value.system?.name ?: "")))
|
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) {
|
fun saveSystem(name: String, location: String, iconName: String, colorName: String) {
|
||||||
val current = state.value.system ?: return
|
val current = state.value.system ?: return
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repo.upsertSystem(current.copy(name = name, location = location, iconName = iconName, colorName = colorName))
|
repo.upsertSystem(current.copy(name = name, location = location, iconName = iconName, colorName = colorName))
|
||||||
Analytics.log("System Updated", mapOf("name" to name))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ fun SystemsScreen(
|
|||||||
SystemsOnboarding(
|
SystemsOnboarding(
|
||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier.padding(padding),
|
||||||
onCreate = { name ->
|
onCreate = { name ->
|
||||||
vm.createSystem(name, source = "onboarding", randomColor = true, onCreated = onOpenSystem)
|
vm.createSystem(name, source = "named", randomColor = true, onCreated = onOpenSystem)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -167,6 +167,11 @@ private fun SystemsOnboarding(modifier: Modifier = Modifier, onCreate: (String)
|
|||||||
val defaultName = stringResource(R.string.default_system_name)
|
val defaultName = stringResource(R.string.default_system_name)
|
||||||
val effective = remember(name) { 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(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Application
|
|||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.voltplan.cable.CableApplication
|
import app.voltplan.cable.CableApplication
|
||||||
|
import app.voltplan.cable.R
|
||||||
import app.voltplan.cable.analytics.Analytics
|
import app.voltplan.cable.analytics.Analytics
|
||||||
import app.voltplan.cable.data.model.ElectricalSystem
|
import app.voltplan.cable.data.model.ElectricalSystem
|
||||||
import app.voltplan.cable.data.model.SavedLoad
|
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. */
|
/** 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) {
|
fun createSystem(preferredName: String?, source: String, randomColor: Boolean, onCreated: (String) -> Unit) {
|
||||||
viewModelScope.launch {
|
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 name = repo.uniqueSystemName(base)
|
||||||
val color = if (randomColor) SystemIconMapper.colorOptions.random() else "blue"
|
val color = if (randomColor) SystemIconMapper.colorOptions.random() else "blue"
|
||||||
val system = ElectricalSystem(
|
val system = ElectricalSystem(
|
||||||
|
|||||||
@@ -263,4 +263,16 @@
|
|||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<string name="component_fallback_name">Komponente</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -263,4 +263,16 @@
|
|||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<string name="component_fallback_name">Componente</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -263,4 +263,16 @@
|
|||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<string name="component_fallback_name">Composant</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -263,4 +263,16 @@
|
|||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<string name="component_fallback_name">Component</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -263,4 +263,16 @@
|
|||||||
|
|
||||||
<!-- Misc -->
|
<!-- Misc -->
|
||||||
<string name="component_fallback_name">Component</string>
|
<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>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user