Skip to content

Commit f6a0cce

Browse files
eriedclaude
andcommitted
release: 0.11.6 (253) — cooldown 0, decimal pills, hold-to-repeat, conversion audit fixes
Alarm cooldown minimum is now 0 (no rate limit): Once = one alert per crossing, Many = re-alert on every reading; help text + 18-locale strings for both 0 cases. NumberUpDown gains decimal/sign support (format/parse/allowSign) and press-and-hold auto-repeat with acceleration on the steppers. Audit of the slider->pill conversions, restoring lost behavior: - Speed calibration: back to 0.1% precision (signed, tenths domain) - Speech speed: back to "1.2x" representation (was shown as %), 0.1 steps - Navigator arrival/off-route: restore imperial ft display for imperial users - (No loss: voice interval, sunset lights, wheel speed limits; idle keeps 30s granularity, now shown in seconds instead of mm:ss) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GADLyChheAoMX9dQbRgRnH
1 parent a2f7213 commit f6a0cce

23 files changed

Lines changed: 170 additions & 55 deletions

File tree

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ android {
2929
applicationId = "com.eried.eucplanet"
3030
minSdk = 29
3131
targetSdk = 35
32-
versionCode = 252
33-
versionName = "0.11.5"
32+
versionCode = 253
33+
versionName = "0.11.6"
3434

3535
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
3636

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

Lines changed: 102 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.imePadding
1616
import androidx.compose.foundation.layout.padding
1717
import androidx.compose.foundation.layout.size
1818
import androidx.compose.foundation.layout.width
19+
import androidx.compose.foundation.gestures.detectTapGestures
1920
import androidx.compose.foundation.rememberScrollState
2021
import androidx.compose.foundation.shape.RoundedCornerShape
2122
import androidx.compose.foundation.text.BasicTextField
@@ -63,12 +64,16 @@ import androidx.compose.runtime.mutableFloatStateOf
6364
import androidx.compose.runtime.mutableIntStateOf
6465
import androidx.compose.runtime.mutableStateOf
6566
import androidx.compose.runtime.remember
67+
import androidx.compose.runtime.rememberCoroutineScope
68+
import androidx.compose.runtime.rememberUpdatedState
6669
import androidx.compose.runtime.setValue
6770
import androidx.compose.ui.Alignment
6871
import androidx.compose.ui.Modifier
6972
import androidx.compose.ui.draw.alpha
7073
import androidx.compose.ui.focus.onFocusChanged
7174
import androidx.compose.ui.graphics.SolidColor
75+
import androidx.compose.ui.input.pointer.pointerInput
76+
import kotlinx.coroutines.launch
7277
import androidx.compose.ui.res.stringResource
7378
import androidx.compose.ui.text.TextStyle
7479
import androidx.compose.ui.text.input.KeyboardType
@@ -740,7 +745,7 @@ private fun AlarmRuleEditorDialog(
740745
NumberUpDown(
741746
value = cooldownSeconds,
742747
onValueChange = { cooldownSeconds = it },
743-
range = 1..120,
748+
range = 0..120,
744749
suffix = "s",
745750
label = stringResource(R.string.alarm_cooldown_label),
746751
modifier = Modifier.weight(1f),
@@ -772,10 +777,19 @@ private fun AlarmRuleEditorDialog(
772777
}
773778
}
774779
HintText(
775-
if (repeatWhileActive)
776-
stringResource(R.string.alarm_repeat_help_many, cooldownSeconds, triggeredPhrase)
777-
else
778-
stringResource(R.string.alarm_repeat_help_once, triggeredPhrase, safePhrase, cooldownSeconds),
780+
when {
781+
// Cooldown 0 = no rate limit. In Many mode that means a
782+
// fresh alert on every telemetry reading while held; in
783+
// Once mode it re-arms the instant the value goes safe.
784+
repeatWhileActive && cooldownSeconds == 0 ->
785+
stringResource(R.string.alarm_repeat_help_many_instant, triggeredPhrase)
786+
repeatWhileActive ->
787+
stringResource(R.string.alarm_repeat_help_many, cooldownSeconds, triggeredPhrase)
788+
cooldownSeconds == 0 ->
789+
stringResource(R.string.alarm_repeat_help_once_instant, triggeredPhrase, safePhrase)
790+
else ->
791+
stringResource(R.string.alarm_repeat_help_once, triggeredPhrase, safePhrase, cooldownSeconds)
792+
},
779793
small = true
780794
)
781795

@@ -924,14 +938,28 @@ internal fun NumberUpDown(
924938
suffix: String = "",
925939
label: String? = null,
926940
enabled: Boolean = true,
941+
// Decimal / signed support: the value stays an Int (the caller scales it,
942+
// e.g. tenths of a percent), [format] renders it for display and [parse]
943+
// turns typed text back into that Int. Defaults keep plain integer behaviour.
944+
format: (Int) -> String = { it.toString() },
945+
parse: (String) -> Int? = { it.toIntOrNull() },
946+
allowSign: Boolean = false,
927947
) {
928948
val fieldText = MaterialTheme.appColors.fieldText
929949
val fieldLabelColor = MaterialTheme.appColors.fieldLabel
930-
var text by remember { mutableStateOf(value.toString()) }
950+
var text by remember { mutableStateOf(format(value)) }
931951
var focused by remember { mutableStateOf(false) }
932952
// Width the typed number to the widest value in range so the unit stays put
933953
// and the digits + unit read as one centred group.
934-
val numWidth = (maxOf(2, range.first.toString().length, range.last.toString().length) * 12).dp
954+
val numWidth = (maxOf(2, format(range.first).length, format(range.last).length) * 12).dp
955+
956+
// rememberUpdatedState so the hold-to-repeat loop below always steps from the
957+
// freshly committed value, not the value captured when the press started.
958+
val latestValue by rememberUpdatedState(value)
959+
fun stepBy(delta: Int) {
960+
val nv = (latestValue + delta).coerceIn(range)
961+
if (nv != latestValue) { text = format(nv); onValueChange(nv) }
962+
}
935963

936964
Column(modifier = modifier.alpha(if (enabled) 1f else 0.5f)) {
937965
if (label != null) {
@@ -952,32 +980,26 @@ internal fun NumberUpDown(
952980
modifier = Modifier.height(48.dp),
953981
verticalAlignment = Alignment.CenterVertically
954982
) {
955-
IconButton(
956-
onClick = {
957-
val nv = (value - step).coerceIn(range)
958-
text = nv.toString(); onValueChange(nv)
959-
},
983+
RepeatingStepper(
984+
icon = Icons.Default.Remove,
985+
contentDescription = stringResource(R.string.alarm_threshold_decrease),
960986
enabled = enabled && value > range.first,
961-
modifier = Modifier.size(48.dp)
962-
) {
963-
Icon(
964-
Icons.Default.Remove,
965-
contentDescription = stringResource(R.string.alarm_threshold_decrease),
966-
tint = fieldText,
967-
modifier = Modifier.size(20.dp)
968-
)
969-
}
987+
tint = fieldText,
988+
onStep = { stepBy(-step) },
989+
)
970990
Row(
971991
modifier = Modifier.weight(1f),
972992
horizontalArrangement = Arrangement.Center,
973993
verticalAlignment = Alignment.CenterVertically
974994
) {
975995
BasicTextField(
976-
value = if (focused) text else value.toString(),
996+
value = if (focused) text else format(value),
977997
onValueChange = { raw ->
978-
val digits = raw.filter { it.isDigit() }.take(6)
979-
text = digits
980-
digits.toIntOrNull()?.let { if (it in range) onValueChange(it) }
998+
val filtered = if (allowSign)
999+
raw.filter { it.isDigit() || it == '-' || it == '.' }.take(7)
1000+
else raw.filter { it.isDigit() }.take(6)
1001+
text = filtered
1002+
parse(filtered)?.let { if (it in range) onValueChange(it) }
9811003
},
9821004
singleLine = true,
9831005
enabled = enabled,
@@ -988,11 +1010,13 @@ internal fun NumberUpDown(
9881010
textAlign = TextAlign.Center
9891011
),
9901012
cursorBrush = SolidColor(fieldText),
991-
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
1013+
keyboardOptions = KeyboardOptions(
1014+
keyboardType = if (allowSign) KeyboardType.Decimal else KeyboardType.Number
1015+
),
9921016
modifier = Modifier
9931017
.width(numWidth)
9941018
.onFocusChanged { f ->
995-
if (f.isFocused) text = value.toString()
1019+
if (f.isFocused) text = format(value)
9961020
focused = f.isFocused
9971021
}
9981022
)
@@ -1001,26 +1025,63 @@ internal fun NumberUpDown(
10011025
Text(suffix, fontSize = 14.sp, color = fieldLabelColor)
10021026
}
10031027
}
1004-
IconButton(
1005-
onClick = {
1006-
val nv = (value + step).coerceIn(range)
1007-
text = nv.toString(); onValueChange(nv)
1008-
},
1028+
RepeatingStepper(
1029+
icon = Icons.Default.Add,
1030+
contentDescription = stringResource(R.string.alarm_threshold_increase),
10091031
enabled = enabled && value < range.last,
1010-
modifier = Modifier.size(48.dp)
1011-
) {
1012-
Icon(
1013-
Icons.Default.Add,
1014-
contentDescription = stringResource(R.string.alarm_threshold_increase),
1015-
tint = fieldText,
1016-
modifier = Modifier.size(20.dp)
1017-
)
1018-
}
1032+
tint = fieldText,
1033+
onStep = { stepBy(step) },
1034+
)
10191035
}
10201036
}
10211037
}
10221038
}
10231039

1040+
/**
1041+
* 48dp icon button for the NumberUpDown steppers: fires [onStep] once on press,
1042+
* then auto-repeats with acceleration while held so the rider can sweep a value
1043+
* fast (handy for fine 0.1-step fields like speed calibration).
1044+
*/
1045+
@Composable
1046+
private fun RepeatingStepper(
1047+
icon: androidx.compose.ui.graphics.vector.ImageVector,
1048+
contentDescription: String,
1049+
enabled: Boolean,
1050+
tint: androidx.compose.ui.graphics.Color,
1051+
onStep: () -> Unit,
1052+
) {
1053+
val step by rememberUpdatedState(onStep)
1054+
val scope = rememberCoroutineScope()
1055+
Box(
1056+
modifier = Modifier
1057+
.size(48.dp)
1058+
.pointerInput(enabled) {
1059+
if (!enabled) return@pointerInput
1060+
detectTapGestures(onPress = {
1061+
step() // immediate first tick
1062+
val job = scope.launch {
1063+
kotlinx.coroutines.delay(400) // hold before auto-repeat
1064+
var d = 260L
1065+
while (true) {
1066+
step()
1067+
kotlinx.coroutines.delay(d)
1068+
d = (d * 80 / 100).coerceAtLeast(40L) // accelerate, floor 40ms
1069+
}
1070+
}
1071+
try { awaitRelease() } finally { job.cancel() }
1072+
})
1073+
},
1074+
contentAlignment = Alignment.Center
1075+
) {
1076+
Icon(
1077+
icon,
1078+
contentDescription = contentDescription,
1079+
tint = if (enabled) tint else tint.copy(alpha = 0.3f),
1080+
modifier = Modifier.size(20.dp)
1081+
)
1082+
}
1083+
}
1084+
10241085
@OptIn(ExperimentalMaterial3Api::class)
10251086
@Composable
10261087
private fun DropdownSelect(

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ fun NavigatorSettingsContent(
7171
fun distLabel(meters: Int): String =
7272
if (settings.imperialUnits) "${(meters * 3.28084).roundToInt()} ft"
7373
else "$meters m"
74+
// Typed text back to stored metres (ft -> m for imperial), snapped to 5 m.
75+
fun distParse(s: String): Int? = s.toIntOrNull()?.let { typed ->
76+
val meters = if (settings.imperialUnits) typed / 3.28084 else typed.toDouble()
77+
(meters / 5.0).roundToInt() * 5
78+
}
7479

7580
Column(
7681
modifier = Modifier.fillMaxWidth(),
@@ -104,18 +109,20 @@ fun NavigatorSettingsContent(
104109
onValueChange = { viewModel.updateNavArrivalRadius(it) },
105110
range = 5..100,
106111
step = 5,
107-
suffix = "m",
108112
label = stringResource(R.string.nav_setting_arrival_radius),
109113
modifier = Modifier.weight(1f),
114+
format = { distLabel(it) },
115+
parse = { distParse(it) },
110116
)
111117
NumberUpDown(
112118
value = settings.navOffRouteToleranceM,
113119
onValueChange = { viewModel.updateNavOffRouteTolerance(it) },
114120
range = 15..150,
115121
step = 5,
116-
suffix = "m",
117122
label = stringResource(R.string.nav_setting_offroute),
118123
modifier = Modifier.weight(1f),
124+
format = { distLabel(it) },
125+
parse = { distParse(it) },
119126
)
120127
}
121128
// Both field hints combined on one line.

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5328,15 +5328,21 @@ private fun SpeedTab(
53285328
modifier = Modifier.fillMaxWidth(),
53295329
horizontalArrangement = Arrangement.spacedBy(8.dp)
53305330
) {
5331+
// Tenths-of-a-percent domain so the pill keeps the slider's 0.1%
5332+
// precision: the Int value is tenths, displayed as a signed 1-decimal
5333+
// percent, stepped 0.1% per tap (hold to sweep).
53315334
NumberUpDown(
5332-
value = calPct.roundToInt(),
5333-
onValueChange = { viewModel.updateSpeedCalibrationOffsetPct(it.toFloat()) },
5334-
range = -15..15,
5335+
value = (calPct * 10).roundToInt(),
5336+
onValueChange = { viewModel.updateSpeedCalibrationOffsetPct(it / 10f) },
5337+
range = -150..150,
53355338
step = 1,
53365339
suffix = "%",
53375340
label = stringResource(R.string.speed_calibration_label),
53385341
enabled = isConnected,
53395342
modifier = Modifier.weight(1f),
5343+
format = { String.format(java.util.Locale.US, "%+.1f", it / 10f) },
5344+
parse = { it.toFloatOrNull()?.let { f -> (f * 10).roundToInt() } },
5345+
allowSign = true,
53405346
)
53415347
Spacer(Modifier.weight(1f))
53425348
}
@@ -5439,16 +5445,19 @@ private fun VoiceTab(
54395445
modifier = Modifier.fillMaxWidth(),
54405446
horizontalArrangement = Arrangement.spacedBy(12.dp)
54415447
) {
5448+
// Tenths-of-x domain so it reads as the original "1.2x" with 0.1
5449+
// steps, not a percentage.
54425450
NumberUpDown(
5443-
value = (settings.voiceSpeechRate * 100).roundToInt(),
5444-
onValueChange = {
5445-
viewModel.updateVoiceSpeechRate((Math.round(it / 10f) * 10).coerceIn(50, 250) / 100f)
5446-
},
5447-
range = 50..250,
5448-
step = 10,
5449-
suffix = "%",
5451+
value = (settings.voiceSpeechRate * 10).roundToInt(),
5452+
onValueChange = { viewModel.updateVoiceSpeechRate(it / 10f) },
5453+
range = 5..25,
5454+
step = 1,
5455+
suffix = stringResource(R.string.unit_x),
54505456
label = stringResource(R.string.voice_speech_speed),
54515457
modifier = Modifier.weight(1f),
5458+
format = { String.format(java.util.Locale.US, "%.1f", it / 10f) },
5459+
parse = { it.toFloatOrNull()?.let { f -> (f * 10).roundToInt() } },
5460+
allowSign = true,
54525461
)
54535462
Spacer(Modifier.weight(1f))
54545463
}

app/src/main/res/values-b+es+419/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,8 @@
987987
<string name="alarm_state_below">%1$s está por debajo de %2$s</string>
988988
<string name="alarm_repeat_help_once">Un aviso cuando %1$s. Se rearma cuando %2$s, tras la pausa de %3$d s.</string>
989989
<string name="alarm_repeat_help_many">Vuelve a avisar cada %1$d s mientras %2$s.</string>
990+
<string name="alarm_repeat_help_once_instant">Un aviso cuando %1$s. Se rearma en cuanto %2$s.</string>
991+
<string name="alarm_repeat_help_many_instant">Vuelve a avisar en cada lectura mientras %1$s.</string>
990992
<string name="alarm_predict_help_now">Avisa en el instante en que %1$s.</string>
991993
<string name="alarm_predict_help_lead">Avisa unos %1$s s antes de que ocurra: %2$s, según la tendencia reciente.</string>
992994

app/src/main/res/values-da/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,8 @@
983983
<string name="alarm_repeat_help">Én gang: én advarsel, og klargøres igen, når aflæsningen vender tilbage til den sikre side.\nMange: advarer igen efter hver pause, så længe den er udløst</string>
984984
<string name="alarm_repeat_help_once">Én advarsel når %1$s. Klargøres igen når %2$s, efter pausen på %3$d s.</string>
985985
<string name="alarm_repeat_help_many">Advarer igen hver %1$d s mens %2$s.</string>
986+
<string name="alarm_repeat_help_once_instant">Én advarsel når %1$s. Klargøres igen så snart %2$s.</string>
987+
<string name="alarm_repeat_help_many_instant">Advarer igen ved hver måling mens %1$s.</string>
986988
<string name="alarm_predict_label">Forudsigende udløser</string>
987989
<string name="alarm_predict_realtime">Nu</string>
988990
<string name="alarm_predict_value_fmt">%1$s s tidligere</string>

app/src/main/res/values-de/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,8 @@
982982
<string name="alarm_state_below">%1$s ist unter %2$s</string>
983983
<string name="alarm_repeat_help_once">Eine Warnung, sobald dies gilt: %1$s. Erneut scharf nach %3$d s Abklingzeit, sobald dies gilt: %2$s.</string>
984984
<string name="alarm_repeat_help_many">Warnt alle %1$d s erneut, solange dies gilt: %2$s.</string>
985+
<string name="alarm_repeat_help_once_instant">Eine Warnung, sobald dies gilt: %1$s. Sofort erneut scharf, sobald dies gilt: %2$s.</string>
986+
<string name="alarm_repeat_help_many_instant">Warnt bei jeder Messung erneut, solange dies gilt: %1$s.</string>
985987
<string name="alarm_predict_help_now">Warnt sofort, sobald dies gilt: %1$s.</string>
986988
<string name="alarm_predict_help_lead">Warnt anhand des aktuellen Trends rund %1$s s, bevor dies gilt: %2$s.</string>
987989
<string name="alarm_predict_label">Vorausschauende Auslösung</string>

app/src/main/res/values-es/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,8 @@
989989
<string name="alarm_state_below">%1$s está por debajo de %2$s</string>
990990
<string name="alarm_repeat_help_once">Un aviso cuando %1$s. Se rearma cuando %2$s, tras la pausa de %3$d s.</string>
991991
<string name="alarm_repeat_help_many">Vuelve a avisar cada %1$d s mientras %2$s.</string>
992+
<string name="alarm_repeat_help_once_instant">Un aviso cuando %1$s. Se rearma en cuanto %2$s.</string>
993+
<string name="alarm_repeat_help_many_instant">Vuelve a avisar en cada lectura mientras %1$s.</string>
992994
<string name="alarm_predict_help_now">Avisa en cuanto %1$s.</string>
993995
<string name="alarm_predict_help_lead">Avisa unos %1$s s antes de %2$s, según la tendencia reciente.</string>
994996

app/src/main/res/values-fr/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,8 @@
989989
<string name="alarm_state_below">%1$s est sous %2$s</string>
990990
<string name="alarm_repeat_help_once">Une alerte quand %1$s. Se réarme quand %2$s, après la pause de %3$d s.</string>
991991
<string name="alarm_repeat_help_many">Réalerte toutes les %1$d s tant que %2$s.</string>
992+
<string name="alarm_repeat_help_once_instant">Une alerte quand %1$s. Se réarme dès que %2$s.</string>
993+
<string name="alarm_repeat_help_many_instant">Réalerte à chaque relevé tant que %1$s.</string>
992994
<string name="alarm_predict_help_now">Alerte dès que %1$s.</string>
993995
<string name="alarm_predict_help_lead">Alerte environ %1$s s avant %2$s, d\'après la tendance récente.</string>
994996

app/src/main/res/values-it/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,8 @@
577577
<string name="alarm_state_below">%1$s è sotto %2$s</string>
578578
<string name="alarm_repeat_help_once">Un solo avviso quando %1$s. Si riarma quando %2$s, dopo la pausa di %3$d s.</string>
579579
<string name="alarm_repeat_help_many">Riavvisa ogni %1$d s finché %2$s.</string>
580+
<string name="alarm_repeat_help_once_instant">Un solo avviso quando %1$s. Si riarma appena %2$s.</string>
581+
<string name="alarm_repeat_help_many_instant">Riavvisa a ogni lettura finché %1$s.</string>
580582
<string name="alarm_predict_help_now">Avvisa nell\'istante in cui %1$s.</string>
581583
<string name="alarm_predict_help_lead">Avvisa circa %1$s s in anticipo, quando %2$s, in base alla tendenza recente.</string>
582584

0 commit comments

Comments
 (0)