Skip to content

Commit e8e13d4

Browse files
committed
Settings polish bundle: scroll nested expansions, name-as-last-backup, period sweep
- BringIntoViewOnFirstShow() helper added; dropped into every conditional expansion on the Settings screen so newly-revealed sub-blocks reveal themselves the same way the top-level CollapsibleSection cards do: autoRecord, autoRecordStartInMotion, voiceEnabled, volumeKeysEnabled, engineSoundEnabled, autoLightsEnabled, autoVolumeEnabled and the Additional GPS sub-section. - "Last backup" label now reads "Last backup: <date> AS <name>" after a long-press named save. Adds lastSettingsBackupName to AppSettings + SettingsJson + stripDeviceBindings, and a new cloud_last_backup_named string localised across 14 locales. Every successful backup updates the timestamp now (default and named), with the name field staying null for the default snapshot. - Backup name dialog: dropped the "Will save as eucplanet_settings-..." preview line; the field label and the dialog title (gone in the prior polish pass) are enough. - Overwrite confirm dialog: Cancel and tap-outside both reopen the name input with the previously-typed name preserved, instead of dismissing the whole flow. - Single-sentence hint / caption / desc strings drop their trailing period across all 15 locale files (perl sweep skips *_body / *_message keys so multi-sentence dialog copy keeps its punctuation). Matches the feel of "Rules are checked top-to-bottom" which already had no period. Phone code 78 -> 79, wear 100079 -> 100080; versionName stays at 0.7.0.
1 parent 57ad918 commit e8e13d4

23 files changed

Lines changed: 537 additions & 470 deletions

File tree

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ android {
2727
applicationId = "com.eried.eucplanet"
2828
minSdk = 29
2929
targetSdk = 35
30-
versionCode = 78
30+
versionCode = 79
3131
versionName = "0.7.0"
3232

3333
val buildStamp = SimpleDateFormat("yyMMdd.HHmm")

app/src/main/java/com/eried/eucplanet/data/model/AppSettings.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ data class AppSettings(
170170
// Backup folder (SAF tree URI on local storage; companion sync app handles cloud upload)
171171
val syncFolderUri: String? = null,
172172
val lastSettingsBackupAt: Long? = null,
173+
/** Snapshot name of the most-recent backup, null for the unnamed default. */
174+
val lastSettingsBackupName: String? = null,
173175

174176
// External BLE GPS pairing (RaceBox today; future Draggy/VBox/etc. share this slot).
175177
// Three values stored: BLE MAC, advertised name (for display), and the source-family

app/src/main/java/com/eried/eucplanet/data/store/SettingsJson.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ object SettingsJson {
3434
externalGpsName = null,
3535
externalGpsSource = null,
3636
syncFolderUri = null,
37-
lastSettingsBackupAt = null
37+
lastSettingsBackupAt = null,
38+
lastSettingsBackupName = null
3839
)
3940

4041
fun toJson(s: AppSettings): JSONObject = JSONObject().apply {
@@ -51,6 +52,7 @@ object SettingsJson {
5152
put("externalGpsSource", s.externalGpsSource)
5253
put("syncFolderUri", s.syncFolderUri)
5354
put("lastSettingsBackupAt", s.lastSettingsBackupAt)
55+
put("lastSettingsBackupName", s.lastSettingsBackupName)
5456

5557
put("tiltbackSpeedKmh", s.tiltbackSpeedKmh)
5658
put("alarmSpeedKmh", s.alarmSpeedKmh)
@@ -183,6 +185,7 @@ object SettingsJson {
183185
lastSettingsBackupAt = if (j.has("lastSettingsBackupAt") && !j.isNull("lastSettingsBackupAt"))
184186
j.optLong("lastSettingsBackupAt", base.lastSettingsBackupAt ?: 0L)
185187
else base.lastSettingsBackupAt,
188+
lastSettingsBackupName = j.optStringOrNull("lastSettingsBackupName", base.lastSettingsBackupName),
186189
tiltbackSpeedKmh = j.optDouble("tiltbackSpeedKmh", base.tiltbackSpeedKmh.toDouble()).toFloat(),
187190
alarmSpeedKmh = j.optDouble("alarmSpeedKmh", base.alarmSpeedKmh.toDouble()).toFloat(),
188191
safetyTiltbackKmh = j.optDouble("safetyTiltbackKmh", base.safetyTiltbackKmh.toDouble()).toFloat(),

app/src/main/java/com/eried/eucplanet/data/sync/SyncManager.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -366,11 +366,15 @@ class SyncManager @Inject constructor(
366366
context.contentResolver.openOutputStream(file.uri)?.use { out ->
367367
out.write(json.toByteArray(Charsets.UTF_8))
368368
} ?: return BackupOutcome.Failed
369-
// Only the default backup updates the "last backup" timestamp so
370-
// named snapshots don't reset the cadence indicator on the dashboard.
371-
if (name == null) {
372-
settingsRepository.update(current.copy(lastSettingsBackupAt = System.currentTimeMillis()))
373-
}
369+
// Every successful backup updates the "last backup" label so the
370+
// rider sees "Last backup: <date> AS <name>" right after a named
371+
// save. Name is null for the default snapshot.
372+
settingsRepository.update(
373+
current.copy(
374+
lastSettingsBackupAt = System.currentTimeMillis(),
375+
lastSettingsBackupName = name
376+
)
377+
)
374378
BackupOutcome.Saved
375379
} catch (e: Exception) {
376380
Log.e(TAG, "Settings backup failed", e)

app/src/main/java/com/eried/eucplanet/ui/settings/AutomationsContent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ fun AutomationsContent(
9595
}
9696

9797
if (settings.autoLightsEnabled) {
98+
BringIntoViewOnFirstShow()
9899
if (autoLightsSuspended) {
99100
Card(colors = CardDefaults.cardColors(containerColor = AccentOrange.copy(alpha = 0.15f))) {
100101
Row(
@@ -203,6 +204,7 @@ fun AutomationsContent(
203204
}
204205

205206
if (settings.autoVolumeEnabled) {
207+
BringIntoViewOnFirstShow()
206208
var points by remember(settings.autoVolumeCurve) {
207209
mutableStateOf(parseVolumeCurve(settings.autoVolumeCurve))
208210
}

app/src/main/java/com/eried/eucplanet/ui/settings/ExternalGpsSection.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ fun ExternalGpsSection(
8585
HintText(stringResource(R.string.gps_log_additional_desc), small = true)
8686

8787
if (!additionalEnabled) return@Column
88+
BringIntoViewOnFirstShow()
8889

8990
ToggleRow(
9091
label = stringResource(R.string.gps_prioritize_external),

app/src/main/java/com/eried/eucplanet/ui/settings/SettingsScreen.kt

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -674,12 +674,14 @@ private fun GeneralTab(
674674
SwitchSetting(stringResource(R.string.auto_record_on_start), settings.autoRecord) { viewModel.updateAutoRecord(it) }
675675
HintText(stringResource(R.string.auto_record_caption), small = true)
676676
if (settings.autoRecord) {
677+
BringIntoViewOnFirstShow()
677678
SwitchSetting(
678679
stringResource(R.string.auto_record_start_in_motion),
679680
settings.autoRecordStartInMotion
680681
) { viewModel.updateAutoRecordStartInMotion(it) }
681682
HintText(stringResource(R.string.auto_record_start_in_motion_caption), small = true)
682683
if (settings.autoRecordStartInMotion) {
684+
BringIntoViewOnFirstShow()
683685
val idleSec = settings.autoRecordStopIdleSeconds
684686
SliderSetting(
685687
label = stringResource(R.string.auto_record_stop_idle_seconds),
@@ -940,6 +942,7 @@ private fun VoiceTab(
940942
}
941943

942944
if (settings.voiceEnabled) {
945+
BringIntoViewOnFirstShow()
943946
SwitchSetting(stringResource(R.string.voice_only_when_connected), settings.voiceOnlyWhenConnected) {
944947
viewModel.updateVoiceOnlyWhenConnected(it)
945948
}
@@ -1256,6 +1259,7 @@ private fun FlicTab(
12561259
settingsViewModel.updateVolumeKeysEnabled(it)
12571260
}
12581261
if (settings.volumeKeysEnabled) {
1262+
BringIntoViewOnFirstShow()
12591263
Card(
12601264
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
12611265
shape = RoundedCornerShape(12.dp)
@@ -1498,6 +1502,7 @@ private fun CloudTab(
14981502
val syncConflict by viewModel.syncConflictPrompt.collectAsState()
14991503
var showRestoreDialog by remember { mutableStateOf(false) }
15001504
var showBackupNameDialog by remember { mutableStateOf(false) }
1505+
var backupNameDraft by remember { mutableStateOf("") }
15011506
var overwritePrompt by remember { mutableStateOf<String?>(null) }
15021507
var showRestorePicker by remember { mutableStateOf(false) }
15031508

@@ -1539,9 +1544,14 @@ private fun CloudTab(
15391544

15401545
if (showBackupNameDialog) {
15411546
NamedBackupDialog(
1542-
onDismiss = { showBackupNameDialog = false },
1547+
initial = backupNameDraft,
1548+
onDismiss = {
1549+
showBackupNameDialog = false
1550+
backupNameDraft = ""
1551+
},
15431552
onSave = { name ->
15441553
showBackupNameDialog = false
1554+
backupNameDraft = name
15451555
viewModel.backupSettingsNamed(name, overwrite = false)
15461556
},
15471557
sanitize = viewModel::sanitizeBackupName
@@ -1550,17 +1560,29 @@ private fun CloudTab(
15501560

15511561
overwritePrompt?.let { pendingName ->
15521562
AlertDialog(
1553-
onDismissRequest = { overwritePrompt = null },
1563+
onDismissRequest = {
1564+
// Treat tap-outside the same as Cancel — return to the name
1565+
// input with the previous text preserved so the rider can
1566+
// adjust the name instead of losing what they typed.
1567+
overwritePrompt = null
1568+
backupNameDraft = pendingName
1569+
showBackupNameDialog = true
1570+
},
15541571
title = { Text(stringResource(R.string.cloud_backup_overwrite_title, pendingName)) },
15551572
text = { Text(stringResource(R.string.cloud_backup_overwrite_body)) },
15561573
confirmButton = {
15571574
Button(onClick = {
15581575
overwritePrompt = null
1576+
backupNameDraft = ""
15591577
viewModel.backupSettingsNamed(pendingName, overwrite = true)
15601578
}) { Text(stringResource(R.string.action_overwrite)) }
15611579
},
15621580
dismissButton = {
1563-
Button(onClick = { overwritePrompt = null }) {
1581+
Button(onClick = {
1582+
overwritePrompt = null
1583+
backupNameDraft = pendingName
1584+
showBackupNameDialog = true
1585+
}) {
15641586
Text(stringResource(R.string.action_cancel))
15651587
}
15661588
}
@@ -1713,9 +1735,15 @@ private fun CloudTab(
17131735

17141736
if (hasFolder) {
17151737
SectionHeader(stringResource(R.string.section_cloud_settings))
1716-
val lastBackupText = settings.lastSettingsBackupAt?.let {
1738+
val lastBackupText = settings.lastSettingsBackupAt?.let { ts ->
17171739
val fmt = java.text.SimpleDateFormat("dd MMM yyyy HH:mm", java.util.Locale.getDefault())
1718-
stringResource(R.string.cloud_last_backup, fmt.format(java.util.Date(it)))
1740+
val date = fmt.format(java.util.Date(ts))
1741+
val named = settings.lastSettingsBackupName
1742+
if (named != null) {
1743+
stringResource(R.string.cloud_last_backup_named, date, named)
1744+
} else {
1745+
stringResource(R.string.cloud_last_backup, date)
1746+
}
17191747
} ?: stringResource(R.string.cloud_last_backup_never)
17201748
Text(lastBackupText, style = MaterialTheme.typography.bodySmall,
17211749
color = MaterialTheme.colorScheme.onSurfaceVariant)
@@ -2265,6 +2293,7 @@ private fun EngineSoundSection(
22652293
)
22662294

22672295
if (settings.engineSoundEnabled) {
2296+
BringIntoViewOnFirstShow()
22682297
// Previews push synthetic telemetry, which would clash with a live ride.
22692298
// Only allow ▶ while the wheel is disconnected or parked.
22702299
EngineTypePicker(
@@ -2887,6 +2916,28 @@ private fun EngineSpeedVolumeCurveEditor(
28872916
}
28882917
}
28892918

2919+
/**
2920+
* Drop this as the FIRST composable inside any conditional / expand-on-toggle
2921+
* block on the Settings screen. A zero-height Spacer carries a
2922+
* BringIntoViewRequester that fires 80 ms after composition so the parent
2923+
* scroll reveals the newly-shown content. The delay covers Compose laying
2924+
* out the rest of the block first.
2925+
*/
2926+
@OptIn(ExperimentalFoundationApi::class)
2927+
@Composable
2928+
internal fun BringIntoViewOnFirstShow() {
2929+
val requester = remember { BringIntoViewRequester() }
2930+
LaunchedEffect(Unit) {
2931+
kotlinx.coroutines.delay(80)
2932+
runCatching { requester.bringIntoView() }
2933+
}
2934+
Spacer(
2935+
modifier = Modifier
2936+
.height(0.dp)
2937+
.bringIntoViewRequester(requester)
2938+
)
2939+
}
2940+
28902941
// --- Long-press backup / restore helpers --------------------------------
28912942

28922943
@OptIn(ExperimentalFoundationApi::class)
@@ -2927,15 +2978,13 @@ private fun LongPressActionButton(
29272978

29282979
@Composable
29292980
private fun NamedBackupDialog(
2981+
initial: String,
29302982
onDismiss: () -> Unit,
29312983
onSave: (String) -> Unit,
29322984
sanitize: (String) -> String?
29332985
) {
2934-
var raw by remember { mutableStateOf("") }
2986+
var raw by remember { mutableStateOf(initial) }
29352987
val sanitized = sanitize(raw)
2936-
val preview = sanitized?.let {
2937-
stringResource(R.string.cloud_backup_name_preview, "eucplanet_settings-$it.json")
2938-
}
29392988
val focusRequester = remember { androidx.compose.ui.focus.FocusRequester() }
29402989
LaunchedEffect(Unit) {
29412990
// Tiny delay lets the dialog finish its mount animation; without it
@@ -2946,24 +2995,15 @@ private fun NamedBackupDialog(
29462995
AlertDialog(
29472996
onDismissRequest = onDismiss,
29482997
text = {
2949-
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
2950-
OutlinedTextField(
2951-
value = raw,
2952-
onValueChange = { raw = it },
2953-
label = { Text(stringResource(R.string.cloud_backup_name_label)) },
2954-
singleLine = true,
2955-
modifier = Modifier
2956-
.fillMaxWidth()
2957-
.focusRequester(focusRequester)
2958-
)
2959-
if (preview != null) {
2960-
Text(
2961-
preview,
2962-
style = MaterialTheme.typography.bodySmall,
2963-
color = MaterialTheme.colorScheme.onSurfaceVariant
2964-
)
2965-
}
2966-
}
2998+
OutlinedTextField(
2999+
value = raw,
3000+
onValueChange = { raw = it },
3001+
label = { Text(stringResource(R.string.cloud_backup_name_label)) },
3002+
singleLine = true,
3003+
modifier = Modifier
3004+
.fillMaxWidth()
3005+
.focusRequester(focusRequester)
3006+
)
29673007
},
29683008
confirmButton = {
29693009
Button(

0 commit comments

Comments
 (0)