diff --git a/android/app/src/main/java/app/voltplan/cable/data/UnitSystem.kt b/android/app/src/main/java/app/voltplan/cable/data/UnitSystem.kt index 382bc5c..c93f372 100644 --- a/android/app/src/main/java/app/voltplan/cable/data/UnitSystem.kt +++ b/android/app/src/main/java/app/voltplan/cable/data/UnitSystem.kt @@ -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 } } diff --git a/android/app/src/main/java/app/voltplan/cable/data/model/Entities.kt b/android/app/src/main/java/app/voltplan/cable/data/model/Entities.kt index 875594f..8602e00 100644 --- a/android/app/src/main/java/app/voltplan/cable/data/model/Entities.kt +++ b/android/app/src/main/java/app/voltplan/cable/data/model/Entities.kt @@ -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, diff --git a/android/app/src/main/java/app/voltplan/cable/library/ComponentLibraryViewModel.kt b/android/app/src/main/java/app/voltplan/cable/library/ComponentLibraryViewModel.kt index f6ab835..ab1cd19 100644 --- a/android/app/src/main/java/app/voltplan/cable/library/ComponentLibraryViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/library/ComponentLibraryViewModel.kt @@ -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 { 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) } diff --git a/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt b/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt index d2d0a5e..990ce96 100644 --- a/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt +++ b/android/app/src/main/java/app/voltplan/cable/pdf/SystemOverviewPdf.kt @@ -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() } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/batteries/BatteryEditorViewModel.kt b/android/app/src/main/java/app/voltplan/cable/ui/batteries/BatteryEditorViewModel.kt index c85f0ca..2613342 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/batteries/BatteryEditorViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/batteries/BatteryEditorViewModel.kt @@ -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 = emptyList() private val _state = MutableStateFlow(BatteryState()) val state: StateFlow = _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)) } } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/chargers/ChargerEditorViewModel.kt b/android/app/src/main/java/app/voltplan/cable/ui/chargers/ChargerEditorViewModel.kt index ba84b25..4ca73e1 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/chargers/ChargerEditorViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/chargers/ChargerEditorViewModel.kt @@ -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 = emptyList() private val _state = MutableStateFlow(ChargerState(inputVoltage = LocaleDefaults.mainsVoltage)) val state: StateFlow = _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)) } } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/components/AppearanceEditor.kt b/android/app/src/main/java/app/voltplan/cable/ui/components/AppearanceEditor.kt index ab8fc92..6aa2534 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/components/AppearanceEditor.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/components/AppearanceEditor.kt @@ -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( diff --git a/android/app/src/main/java/app/voltplan/cable/ui/components/SnapSlider.kt b/android/app/src/main/java/app/voltplan/cable/ui/components/SnapSlider.kt index 60a539f..8a7c787 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/components/SnapSlider.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/components/SnapSlider.kt @@ -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)) } }, ) } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/library/ComponentLibraryScreen.kt b/android/app/src/main/java/app/voltplan/cable/ui/library/ComponentLibraryScreen.kt index 2b81253..e35ff82 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/library/ComponentLibraryScreen.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/library/ComponentLibraryScreen.kt @@ -67,13 +67,12 @@ fun ComponentLibraryScreen( val state by vm.state.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - Analytics.log( - "Component Library Opened", - mapOf( - "source" to if (targetSystemId != null) "system" else "systems-list", - "type" to libraryType.typeValue, - ), + val props = mutableMapOf( + "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( diff --git a/android/app/src/main/java/app/voltplan/cable/ui/loads/CalculatorViewModel.kt b/android/app/src/main/java/app/voltplan/cable/ui/loads/CalculatorViewModel.kt index 4c5e87b..f8b6290 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/loads/CalculatorViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/loads/CalculatorViewModel.kt @@ -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,9 +74,13 @@ class CalculatorViewModel( } } } else { - // Mirror SystemComponentsPersistence default load values. - _state.value = CalcState() - persist(created = true) + // 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) + } } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt index a7c77ac..adebffe 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailScreen.kt @@ -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 + }, ) } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt index aa24d41..f1da298 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/system/SystemDetailViewModel.kt @@ -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)) } } diff --git a/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt b/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt index 50660ee..0374e8c 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsScreen.kt @@ -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() diff --git a/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsViewModel.kt b/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsViewModel.kt index bd4e8dd..a3eecd6 100644 --- a/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsViewModel.kt +++ b/android/app/src/main/java/app/voltplan/cable/ui/systems/SystemsViewModel.kt @@ -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().getString(R.string.default_system_new) val name = repo.uniqueSystemName(base) val color = if (randomColor) SystemIconMapper.colorOptions.random() else "blue" val system = ElectricalSystem( diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml index 8f0f991..0004d5b 100644 --- a/android/app/src/main/res/values-de/strings.xml +++ b/android/app/src/main/res/values-de/strings.xml @@ -263,4 +263,16 @@ Komponente + + + Abbrechen + Symbol + Farbe + Kabellänge + Leistungsverlust + Nutzbare Energie + Neuer Verbraucher + Neue Batterie + Neues Ladegerät + Neues System diff --git a/android/app/src/main/res/values-es/strings.xml b/android/app/src/main/res/values-es/strings.xml index 9b7e249..9d73322 100644 --- a/android/app/src/main/res/values-es/strings.xml +++ b/android/app/src/main/res/values-es/strings.xml @@ -263,4 +263,16 @@ Componente + + + Cancelar + Icono + Color + Longitud del cable + Pérdida de potencia + Energía utilizable + Carga nueva + Nueva batería + Nuevo cargador + Sistema nuevo diff --git a/android/app/src/main/res/values-fr/strings.xml b/android/app/src/main/res/values-fr/strings.xml index c3fef4d..222381c 100644 --- a/android/app/src/main/res/values-fr/strings.xml +++ b/android/app/src/main/res/values-fr/strings.xml @@ -263,4 +263,16 @@ Composant + + + Annuler + Icône + Couleur + Longueur du câble + Perte de puissance + Énergie utilisable + Nouvelle charge + Nouvelle batterie + Nouveau chargeur + Nouveau système diff --git a/android/app/src/main/res/values-nl/strings.xml b/android/app/src/main/res/values-nl/strings.xml index 6d25dd0..9eaa7d5 100644 --- a/android/app/src/main/res/values-nl/strings.xml +++ b/android/app/src/main/res/values-nl/strings.xml @@ -263,4 +263,16 @@ Component + + + Annuleren + Pictogram + Kleur + Kabellengte + Vermogensverlies + Bruikbare energie + Nieuwe last + Nieuwe batterij + Nieuwe lader + Nieuw systeem diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 754518d..9de018c 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -263,4 +263,16 @@ Component + + + Cancel + Icon + Color + Cable Length + Power Loss + Usable Energy + New Load + New Battery + New Charger + New System