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:
1 parent
face2edb8d
commit
cac5b2dfca
18 files changed
+55
-101
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user