refactor(settings): remove the unused default timer duration
Settings > Timers offered a "Default duration", but nothing that creates a timer ever read it: the keypad starts empty. The row, its preference, ClockDefaults.timerDuration and the backup field are gone, and the screen's hint and the hub's subtitle now only speak of the sound. Backups no longer write default_timer_duration_millis. Older files that carry it still import, since the codec ignores unknown keys. Closes #5
This commit is contained in:
@@ -76,7 +76,6 @@ class BackupEngine @Inject constructor(
|
||||
settings.setAlarmRingtoneUri(backup.defaults.alarmRingtoneUri)
|
||||
settings.setTimerRingtoneUri(backup.defaults.timerRingtoneUri)
|
||||
settings.setDismissChallenge(backup.defaults.dismissChallenge)
|
||||
settings.setTimerDuration(backup.defaults.timerDuration)
|
||||
settings.setHomeZoneId(backup.defaults.homeZoneId)
|
||||
settings.setTimerPresets(backup.timerPresets)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,6 @@ object BackupCodec {
|
||||
alarmRingtoneUri = defaults.alarmRingtoneUri,
|
||||
timerRingtoneUri = defaults.timerRingtoneUri,
|
||||
dismissChallenge = defaults.dismissChallenge.name,
|
||||
timerDurationMillis = defaults.timerDuration.inWholeMilliseconds,
|
||||
homeZoneId = defaults.homeZoneId,
|
||||
timerPresets = timerPresets.map { it.inWholeMilliseconds },
|
||||
)
|
||||
@@ -175,7 +174,6 @@ object BackupCodec {
|
||||
alarmRingtoneUri = settingsDto.alarmRingtoneUri?.takeIf { it.isNotBlank() },
|
||||
timerRingtoneUri = settingsDto.timerRingtoneUri?.takeIf { it.isNotBlank() },
|
||||
dismissChallenge = settingsDto.dismissChallenge.toEnum(DismissChallenge.NONE),
|
||||
timerDuration = settingsDto.timerDurationMillis.milliseconds,
|
||||
homeZoneId = settingsDto.homeZoneId?.let(Zones::normalise),
|
||||
),
|
||||
timerPresets = settingsDto.timerPresets.map { it.milliseconds },
|
||||
|
||||
@@ -54,7 +54,11 @@ data class WorldClockDto(
|
||||
@SerialName("sort_order") val sortOrder: Int = 0,
|
||||
)
|
||||
|
||||
/** Field names mirror `ClockPrefs`'/`SettingsPrefs`' on-disk keys. */
|
||||
/**
|
||||
* Field names mirror `ClockPrefs`'/`SettingsPrefs`' on-disk keys. Older files
|
||||
* may still carry `default_timer_duration_millis`, a setting that no longer
|
||||
* exists; the codec ignores unknown keys, so they still import.
|
||||
*/
|
||||
@Serializable
|
||||
data class SettingsDto(
|
||||
@SerialName("theme_mode") val themeMode: String = "SYSTEM",
|
||||
@@ -66,7 +70,6 @@ data class SettingsDto(
|
||||
@SerialName("default_alarm_ringtone_uri") val alarmRingtoneUri: String? = null,
|
||||
@SerialName("default_timer_ringtone_uri") val timerRingtoneUri: String? = null,
|
||||
@SerialName("default_dismiss_challenge") val dismissChallenge: String = "NONE",
|
||||
@SerialName("default_timer_duration_millis") val timerDurationMillis: Long = 300_000L,
|
||||
@SerialName("home_zone_id") val homeZoneId: String? = null,
|
||||
@SerialName("timer_presets") val timerPresets: List<Long> = emptyList(),
|
||||
)
|
||||
|
||||
@@ -6,13 +6,7 @@ import de.jeanlucmakiola.floret.prefs.Pref
|
||||
import de.jeanlucmakiola.floret.prefs.booleanPref
|
||||
import de.jeanlucmakiola.floret.prefs.enumPref
|
||||
import de.jeanlucmakiola.floret.prefs.intPref
|
||||
import de.jeanlucmakiola.floret.prefs.longPref
|
||||
import de.jeanlucmakiola.floret.prefs.nullableStringPref
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The clock defaults' on-disk keys. Reads and writes both clamp through
|
||||
@@ -21,9 +15,6 @@ import kotlin.time.Duration.Companion.seconds
|
||||
*/
|
||||
object ClockPrefs {
|
||||
|
||||
private val MIN_TIMER_DURATION = 1.seconds
|
||||
private val MAX_TIMER_DURATION = 24.hours
|
||||
|
||||
val snoozeMinutes: Pref<Int> = intPref("default_snooze_minutes", 10).clampedTo(1..60)
|
||||
|
||||
val snoozeLimit: Pref<Int> = intPref("default_snooze_limit", 3).clampedTo(0..10)
|
||||
@@ -39,14 +30,6 @@ object ClockPrefs {
|
||||
val dismissChallenge: Pref<DismissChallenge> =
|
||||
enumPref("default_dismiss_challenge", DismissChallenge.NONE)
|
||||
|
||||
val timerDurationMillis: Pref<Duration> = longPref(
|
||||
"default_timer_duration_millis",
|
||||
5.minutes.inWholeMilliseconds,
|
||||
).map(
|
||||
decode = { it.milliseconds.coerceIn(MIN_TIMER_DURATION, MAX_TIMER_DURATION) },
|
||||
encode = { it.coerceIn(MIN_TIMER_DURATION, MAX_TIMER_DURATION).inWholeMilliseconds },
|
||||
)
|
||||
|
||||
/** Normalised through [Zones] both ways, so a zone the device dropped reads as absent. */
|
||||
val homeZoneId: Pref<String?> = nullableStringPref("home_zone_id")
|
||||
.map(decode = Zones::normalise, encode = Zones::normalise)
|
||||
|
||||
@@ -62,13 +62,11 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) {
|
||||
},
|
||||
store.flow(ClockPrefs.alarmRingtoneUri),
|
||||
store.flow(ClockPrefs.timerRingtoneUri),
|
||||
store.flow(ClockPrefs.timerDurationMillis),
|
||||
store.flow(ClockPrefs.homeZoneId),
|
||||
) { partial, alarmRingtoneUri, timerRingtoneUri, timerDuration, homeZoneId ->
|
||||
) { partial, alarmRingtoneUri, timerRingtoneUri, homeZoneId ->
|
||||
partial.copy(
|
||||
alarmRingtoneUri = alarmRingtoneUri,
|
||||
timerRingtoneUri = timerRingtoneUri,
|
||||
timerDuration = timerDuration,
|
||||
homeZoneId = homeZoneId,
|
||||
)
|
||||
}.distinctUntilChanged()
|
||||
@@ -90,8 +88,6 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) {
|
||||
suspend fun setDismissChallenge(challenge: DismissChallenge) =
|
||||
store.set(ClockPrefs.dismissChallenge, challenge)
|
||||
|
||||
suspend fun setTimerDuration(duration: Duration) = store.set(ClockPrefs.timerDurationMillis, duration)
|
||||
|
||||
suspend fun setHomeZoneId(zoneId: String?) = store.set(ClockPrefs.homeZoneId, zoneId)
|
||||
|
||||
// --- timer presets (M6) ---
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package de.jeanlucmakiola.clockula.domain
|
||||
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* The app-wide defaults an alarm or timer inherits when it has no override.
|
||||
* Backed by DataStore, edited in M10's settings screen.
|
||||
@@ -15,6 +12,5 @@ data class ClockDefaults(
|
||||
val alarmRingtoneUri: String? = null,
|
||||
val timerRingtoneUri: String? = null,
|
||||
val dismissChallenge: DismissChallenge = DismissChallenge.NONE,
|
||||
val timerDuration: Duration = 5.minutes,
|
||||
val homeZoneId: String? = null,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ object TimerPresets {
|
||||
val BUILT_IN: List<Duration> =
|
||||
listOf(1.minutes, 2.minutes, 3.minutes, 5.minutes, 10.minutes, 15.minutes, 30.minutes, 1.hours)
|
||||
|
||||
/** `ClockPrefs`' own clamp, so nothing the user can choose is clamped away on the next read. */
|
||||
/** The range a timer can run for: what the keypad can start and what a preset can hold. */
|
||||
val MIN: Duration = 1.seconds
|
||||
|
||||
val MAX: Duration = 24.hours
|
||||
|
||||
@@ -206,7 +206,7 @@ private fun SettingsHub(
|
||||
)
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_section_timers),
|
||||
summary = stringResource(R.string.settings_timers_subtitle),
|
||||
summary = stringResource(R.string.settings_timers_sound_subtitle),
|
||||
position = Position.Middle,
|
||||
leading = { CategoryIcon(Icons.Default.Timer, ChipAccent.Tertiary) },
|
||||
onClick = { onOpenSection(SettingsSection.Timers) },
|
||||
|
||||
@@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
@@ -139,8 +138,6 @@ class SettingsViewModel @Inject internal constructor(
|
||||
fun onDismissChallengeChanged(value: DismissChallenge) =
|
||||
viewModelScope.launch { settings.setDismissChallenge(value) }
|
||||
|
||||
fun onTimerDurationChanged(value: Duration) = viewModelScope.launch { settings.setTimerDuration(value) }
|
||||
|
||||
// --- the default ringtone pickers ---
|
||||
|
||||
internal fun onOpenRingtonePicker(kind: DefaultRingtoneKind) {
|
||||
|
||||
@@ -3,63 +3,30 @@ package de.jeanlucmakiola.clockula.ui.settings
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.clockula.R
|
||||
import de.jeanlucmakiola.clockula.domain.timer.TimerPresets
|
||||
import de.jeanlucmakiola.floret.components.CollapsingScaffold
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.OptionPicker
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
import kotlin.time.Duration
|
||||
|
||||
/** What a new timer starts with: its sound and its length. */
|
||||
/** What a new timer starts with: its sound. */
|
||||
@Composable
|
||||
internal fun TimerDefaultsScreen(
|
||||
state: SettingsUiState.Loaded,
|
||||
viewModel: SettingsViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
var showDuration by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
CollapsingScaffold(title = stringResource(R.string.settings_section_timers), onBack = onBack) {
|
||||
SettingsHint(stringResource(R.string.settings_timer_defaults_hint))
|
||||
SettingsHint(stringResource(R.string.settings_timer_sound_hint))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.editor_ringtone),
|
||||
summary = ringtoneSummary(state.defaults.timerRingtoneUri),
|
||||
position = Position.Top,
|
||||
position = Position.Alone,
|
||||
onClick = { viewModel.onOpenRingtonePicker(DefaultRingtoneKind.TIMER) },
|
||||
)
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_timer_duration),
|
||||
summary = durationMinutesText(state.defaults.timerDuration),
|
||||
position = Position.Bottom,
|
||||
onClick = { showDuration = true },
|
||||
)
|
||||
}
|
||||
|
||||
if (showDuration) {
|
||||
OptionPicker(
|
||||
title = stringResource(R.string.settings_timer_duration),
|
||||
options = TimerPresets.BUILT_IN,
|
||||
selected = state.defaults.timerDuration,
|
||||
label = { durationMinutesText(it) },
|
||||
onSelect = viewModel::onTimerDurationChanged,
|
||||
onDismiss = { showDuration = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun durationMinutesText(duration: Duration): String {
|
||||
val minutes = duration.inWholeMinutes.toInt()
|
||||
return pluralStringResource(R.plurals.settings_duration_minutes, minutes, minutes)
|
||||
}
|
||||
|
||||
@@ -173,9 +173,7 @@ internal fun StartRow(
|
||||
}
|
||||
FilledIconButton(
|
||||
onClick = onStart,
|
||||
// Nothing outside 1 s…24 h is startable, which is `ClockPrefs`'
|
||||
// own clamp — so nothing the user can choose is clamped away on
|
||||
// the next read (M5 D16).
|
||||
// Nothing outside 1 s…24 h (`TimerPresets.MIN..MAX`) is startable.
|
||||
enabled = entry.canStart,
|
||||
modifier = Modifier
|
||||
.size(StartButtonSize)
|
||||
|
||||
@@ -52,7 +52,7 @@ class TimersViewModel @Inject internal constructor(
|
||||
/** Creates a timer of [duration] and starts it. Re-entrant taps create one timer (D23). */
|
||||
fun onStartNew(duration: Duration) {
|
||||
if (creating) return
|
||||
// Outside `ClockPrefs`' own clamp there is nothing honest to create: the
|
||||
// Outside `TimerPresets.MIN..MAX` there is nothing honest to create: the
|
||||
// panel's Start is disabled there, so this is the stale-caller guard.
|
||||
if (duration !in TimerPresets.MIN..TimerPresets.MAX) return
|
||||
creating = true
|
||||
|
||||
@@ -294,13 +294,8 @@
|
||||
<string name="settings_alarms_subtitle">Sound, snooze and dismiss for new alarms</string>
|
||||
<string name="settings_alarm_defaults_hint">New alarms start with these. Each alarm can still change them in its editor.</string>
|
||||
<string name="settings_section_timers">Timers</string>
|
||||
<string name="settings_timers_subtitle">Sound and length for new timers</string>
|
||||
<string name="settings_timer_defaults_hint">New timers start with these.</string>
|
||||
<string name="settings_timer_duration">Default duration</string>
|
||||
<plurals name="settings_duration_minutes">
|
||||
<item quantity="one">%1$d minute</item>
|
||||
<item quantity="other">%1$d minutes</item>
|
||||
</plurals>
|
||||
<string name="settings_timers_sound_subtitle">Sound for new timers</string>
|
||||
<string name="settings_timer_sound_hint">New timers ring with this sound. Each timer can still change it in its editor.</string>
|
||||
<string name="settings_section_clock">Clock</string>
|
||||
<string name="settings_clock_subtitle">Home time zone</string>
|
||||
<string name="settings_clock_hint">World clocks show how far each city is from your home time zone.</string>
|
||||
|
||||
@@ -58,7 +58,6 @@ class BackupCodecTest {
|
||||
alarmRingtoneUri = "content://media/1",
|
||||
timerRingtoneUri = "content://media/2",
|
||||
dismissChallenge = DismissChallenge.HOLD,
|
||||
timerDuration = 10.minutes,
|
||||
homeZoneId = "America/New_York",
|
||||
),
|
||||
timerPresets = listOf(1.minutes, 2.minutes, 10.minutes),
|
||||
@@ -113,6 +112,32 @@ class BackupCodecTest {
|
||||
assertThat(decoded).isInstanceOf(BackupResult.Success::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an older file with the retired default timer duration still imports`() {
|
||||
val json = """
|
||||
{
|
||||
"format": "clockula-backup",
|
||||
"version": 1,
|
||||
"exportedAt": 0,
|
||||
"alarms": [],
|
||||
"timers": [],
|
||||
"worldClocks": [],
|
||||
"settings": { "default_snooze_minutes": 7, "default_timer_duration_millis": 600000 }
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
val decoded = BackupCodec.decode(json)
|
||||
|
||||
assertThat(decoded).isInstanceOf(BackupResult.Success::class.java)
|
||||
val defaults = (decoded as BackupResult.Success).content.settings.defaults
|
||||
assertThat(defaults).isEqualTo(ClockDefaults(snoozeMinutes = 7))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the retired default timer duration is no longer written`() {
|
||||
assertThat(BackupCodec.encode(sampleContent())).doesNotContain("default_timer_duration_millis")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wrong format rejects the whole file`() {
|
||||
val decoded = BackupCodec.decode(minimalDocument(format = "something-else"))
|
||||
|
||||
@@ -20,7 +20,6 @@ import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* The clock defaults every alarm and timer inherits. A value the user could
|
||||
@@ -53,7 +52,6 @@ class ClockPrefsTest {
|
||||
prefs.setAlarmRingtoneUri("content://a")
|
||||
prefs.setTimerRingtoneUri("content://t")
|
||||
prefs.setDismissChallenge(DismissChallenge.HOLD)
|
||||
prefs.setTimerDuration(90.seconds)
|
||||
prefs.setHomeZoneId("Europe/Berlin")
|
||||
|
||||
assertThat(prefs.defaults.first()).isEqualTo(
|
||||
@@ -65,7 +63,6 @@ class ClockPrefsTest {
|
||||
alarmRingtoneUri = "content://a",
|
||||
timerRingtoneUri = "content://t",
|
||||
dismissChallenge = DismissChallenge.HOLD,
|
||||
timerDuration = 90.seconds,
|
||||
homeZoneId = "Europe/Berlin",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* The real [SettingsViewModel] over the real [de.jeanlucmakiola.clockula.data.prefs.SettingsPrefs]
|
||||
@@ -120,7 +119,6 @@ class SettingsViewModelTest {
|
||||
viewModel.onSnoozeLimitChanged(2)
|
||||
viewModel.onVolumeRampChanged(30)
|
||||
viewModel.onDismissChallengeChanged(DismissChallenge.MATH)
|
||||
viewModel.onTimerDurationChanged(20.minutes)
|
||||
advanceUntilIdle()
|
||||
|
||||
val defaults = (viewModel.state.value as SettingsUiState.Loaded).defaults
|
||||
@@ -129,7 +127,6 @@ class SettingsViewModelTest {
|
||||
assertThat(defaults.snoozeLimit).isEqualTo(2)
|
||||
assertThat(defaults.volumeRampSeconds).isEqualTo(30)
|
||||
assertThat(defaults.dismissChallenge).isEqualTo(DismissChallenge.MATH)
|
||||
assertThat(defaults.timerDuration).isEqualTo(20.minutes)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -597,7 +597,7 @@ Four slices:
|
||||
| Slice | Keys | Owner |
|
||||
|---|---|---|
|
||||
| Appearance (M1) | `theme_mode`, `dynamic_color` | `AppearancePrefs` in `core-prefs` — the family's shared key names |
|
||||
| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `default_timer_duration_millis`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
|
||||
| Clock defaults (M2) | `default_snooze_minutes`, `default_snooze_limit`, `default_vibrate`, `default_volume_ramp_seconds`, `default_alarm_ringtone_uri`, `default_timer_ringtone_uri`, `default_dismiss_challenge`, `home_zone_id` | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
|
||||
| Stopwatch run record (M2) | `stopwatch_state`, `stopwatch_started_elapsed_millis`, `stopwatch_accumulated_millis`, `stopwatch_last_lap_cumulative_millis` — the last of which M7 is the first caller of, for the in-progress lap's split | `StopwatchPrefs`, behind `StopwatchStateStore` |
|
||||
| System facts (M3) | `last_boot_id` | `SystemPrefs`, behind `BootStateStore` — the persisted half of the reboot gate |
|
||||
| Timers (M6) | `timer_presets`, `timer_ring_sounding_since_millis` | `TimerPrefs` — the presets surfaced as `SettingsPrefs.timerPresets`, the ring session's anchor behind `TimerRingStateStore` |
|
||||
@@ -612,7 +612,7 @@ process killed mid-write — ever sees `RUNNING` without its start anchor.
|
||||
|
||||
**Clamp on read as well as on write.** Every bounded value goes through
|
||||
`Pref.map`, which applies the same clamp in both directions: snooze 1–60
|
||||
minutes, snooze limit 0–10, volume ramp 0–60 s, timer duration 1 s–24 h. A
|
||||
minutes, snooze limit 0–10, volume ramp 0–60 s. A
|
||||
hand-edited file or a restore from a future version therefore cannot produce a
|
||||
value the user could never have chosen. Unknown enum names degrade to their
|
||||
default; a `home_zone_id` the device's tzdata no longer knows reads back as
|
||||
@@ -1063,7 +1063,7 @@ M10 adds no `<uses-permission>`.
|
||||
| Reordering timers by hand. `sort_order` and `TimerRepository.reorder` exist and stay caller-less: the list is in storage order, M5 gave the same answer for alarms, and a drag surface would also mean abandoning the `LazyColumn` | not planned for v1 |
|
||||
| Per-timer vibrate, volume-ramp or dismiss-challenge overrides. `timers` has exactly one nullable settings column, its ringtone; four more would be a schema change for settings the roadmap does not ask for | not planned for v1 |
|
||||
| A "stop all" for several expired timers, and undo after a timer delete. Each expired timer is acknowledged on its own, and the delete confirmation asks before rather than after | not planned for v1 |
|
||||
| A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_duration_millis` and `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 |
|
||||
| A presets management screen (add/remove the quick-start durations themselves). M10's settings screen edits `default_timer_ringtone_uri`, but the preset list is still only editable from the two-gesture surface on the setup panel | not planned for v1 |
|
||||
| A timer that ramps its volume, a dismiss challenge or a snooze for a timer | not planned for v1 |
|
||||
| Surfacing `TimerSnapshot.anchorIsStale` in the UI. The boot repair makes it vanishingly rare and the value is already honest; a row does not say "estimated" in v1 | not planned for v1 |
|
||||
| Lap export, sharing, deletion or editing, and any "previous run" history. A run's laps belong to the run that produced them; keeping old runs is a different feature with its own table | not planned for v1 |
|
||||
@@ -2018,16 +2018,17 @@ death; the pure `TimerDurationEntry` is derived from the saved digit string, and
|
||||
digits in from the right and reads them as `h*3600 + m*60 + s` **without
|
||||
clamping minutes or seconds to 59**, so `0:00:99` is 99 seconds — Google Clock's
|
||||
forgiving behaviour, and a pure function with obvious tests. `canStart` is false
|
||||
outside 1 s…24 h, which is `ClockPrefs`' own clamp, so nothing the user can
|
||||
choose is clamped away on the next read.
|
||||
outside 1 s…24 h.
|
||||
|
||||
Presets are **app-wide** quick-start durations in DataStore (§6), not a property
|
||||
of one row: a `timers` row already *is* its own preset, because it persists and
|
||||
`reset` returns it to its configured duration. The whole management surface is
|
||||
two gestures on the setup panel — a Save chip when the typed duration is not
|
||||
already one, and each chip's own remove affordance. A new timer's pre-filled
|
||||
duration is `ClockDefaults.timerDuration`, the `default_timer_duration_millis`
|
||||
preference M2 built and nothing had called until now.
|
||||
already one, and each chip's own remove affordance. The keypad starts empty;
|
||||
there is no default timer length. (M2 built a `default_timer_duration_millis`
|
||||
preference and M10 gave it a settings row, but nothing that creates a timer
|
||||
ever read it, so it was removed. A backup file that still carries the key
|
||||
imports with it ignored.)
|
||||
|
||||
A timer has exactly **one** per-setting override, its ringtone, because `timers`
|
||||
has exactly one nullable settings column. Vibrate and the (zero) ramp come from
|
||||
|
||||
+5
-3
@@ -33,7 +33,7 @@ written here.
|
||||
defines — `format`, `version`, an alarm's `hour`/`minute`, a world clock's
|
||||
`zone_id`, a timer's `duration_millis` — rejects the **whole file** with a
|
||||
specific reason. Nothing is imported half-way.
|
||||
- Settings values (snooze minutes, volume ramp, timer duration, …) are
|
||||
- Settings values (snooze minutes, volume ramp, …) are
|
||||
**not** range-checked at parse time; they pass through the same clamps
|
||||
`ClockPrefs`/`SettingsPrefs` already apply on every write, so a value from
|
||||
a future, more permissive version degrades to the nearest valid one instead
|
||||
@@ -118,13 +118,15 @@ device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
||||
"default_alarm_ringtone_uri": null,
|
||||
"default_timer_ringtone_uri": null,
|
||||
"default_dismiss_challenge": "NONE",
|
||||
"default_timer_duration_millis": 300000,
|
||||
"home_zone_id": null,
|
||||
"timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000]
|
||||
}
|
||||
```
|
||||
|
||||
Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Every
|
||||
Field names mirror `ClockPrefs`'/`SettingsPrefs`' own on-disk keys. Older
|
||||
files may also carry `default_timer_duration_millis`, from a default timer
|
||||
length setting that has since been removed; it is ignored on import like any
|
||||
other unknown field. Every
|
||||
numeric/duration value is clamped on *apply*, through the existing pref
|
||||
setters — never on parse.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user