feat(world-clock): an optional compact home clock
A "Compact home clock" switch under Settings > Clock swaps the World clock tab's analog face for a row: a home glyph, the city, "Home · <zone name>" and the time, in the seconds-aware style the city rows use and spoken to the minute like them. It is drawn the way the automatic local clock's row is, through one shared pinned-row composable, and the local row still sits directly under home. Off by default. Like show-seconds it is display-only, so it is its own preference and flow (world_clock_compact_home) rather than a clock default, and it is applied outside the tick so flipping it does not restart the ticker. Backups carry it; older files restore it as off. Closes #29.
This commit is contained in:
1 parent
005df7db19
commit
7d65b2b206
22 files changed
+212
-26
No files matched your search
@@ -116,6 +116,7 @@ class BackupEngine @Inject constructor(
|
||||
defaults = settings.currentDefaults(),
|
||||
timerPresets = settings.timerPresets.first(),
|
||||
worldClockSeconds = settings.worldClockSeconds.first(),
|
||||
compactHomeClock = settings.compactHomeClock.first(),
|
||||
screenSaver = settings.screenSaver.first(),
|
||||
quickTileDuration = settings.quickTileDuration.first(),
|
||||
bedtime = settings.currentBedtime(),
|
||||
|
||||
@@ -148,6 +148,7 @@ object BackupCodec {
|
||||
volumeButtons = defaults.volumeButtons.name,
|
||||
automaticLocalClock = defaults.automaticLocalClock,
|
||||
worldClockSeconds = worldClockSeconds,
|
||||
compactHomeClock = compactHomeClock,
|
||||
timerPresets = timerPresets.map { it.inWholeMilliseconds },
|
||||
bedtime = bedtime?.toDto(),
|
||||
screenSaverStyle = screenSaver.style.name,
|
||||
@@ -311,6 +312,7 @@ object BackupCodec {
|
||||
),
|
||||
timerPresets = settingsDto.timerPresets?.map { it.milliseconds } ?: TimerPresets.BUILT_IN,
|
||||
worldClockSeconds = settingsDto.worldClockSeconds,
|
||||
compactHomeClock = settingsDto.compactHomeClock,
|
||||
screenSaver = ScreenSaverSettings(
|
||||
style = settingsDto.screenSaverStyle.toEnum(ScreenSaverStyle.DIGITAL),
|
||||
nightMode = settingsDto.screenSaverNightMode,
|
||||
|
||||
@@ -97,6 +97,8 @@ data class SettingsDto(
|
||||
@SerialName("automatic_local_clock") val automaticLocalClock: Boolean = true,
|
||||
/** Absent in files from before it existed, which keeps the default: off. */
|
||||
@SerialName("world_clock_show_seconds") val worldClockSeconds: Boolean = false,
|
||||
/** Absent in files from before #29, which keeps the default: off. */
|
||||
@SerialName("world_clock_compact_home") val compactHomeClock: Boolean = false,
|
||||
/**
|
||||
* Absent is not empty: an empty list is a user who removed every preset,
|
||||
* while a file without the field (an older or hand-written one) keeps the
|
||||
|
||||
@@ -74,6 +74,13 @@ object ClockPrefs {
|
||||
*/
|
||||
val worldClockSeconds: Pref<Boolean> = booleanPref("world_clock_show_seconds", false)
|
||||
|
||||
/**
|
||||
* The world clock tab's home as a row like the cities' rather than the
|
||||
* analog face (#29). Off by default. Not part of `ClockDefaults`, for the
|
||||
* same reason as [worldClockSeconds].
|
||||
*/
|
||||
val compactHomeClock: Pref<Boolean> = booleanPref("world_clock_compact_home", false)
|
||||
|
||||
/**
|
||||
* Whether a ringing alarm or timer opens its screen over another app
|
||||
* rather than as a heads-up. Off by default, and only in effect while the
|
||||
|
||||
@@ -128,6 +128,7 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) {
|
||||
prefs[ClockPrefs.volumeButtons] = backup.defaults.volumeButtons
|
||||
prefs[ClockPrefs.automaticLocalClock] = backup.defaults.automaticLocalClock
|
||||
prefs[ClockPrefs.worldClockSeconds] = backup.worldClockSeconds
|
||||
prefs[ClockPrefs.compactHomeClock] = backup.compactHomeClock
|
||||
prefs[TimerPrefs.presets] = backup.timerPresets
|
||||
// Absent from a file written before bedtime existed: keep it.
|
||||
backup.bedtime?.let { prefs.putBedtime(it) }
|
||||
@@ -191,6 +192,11 @@ class SettingsPrefs @Inject constructor(private val store: PrefStore) {
|
||||
|
||||
suspend fun setWorldClockSeconds(enabled: Boolean) = store.set(ClockPrefs.worldClockSeconds, enabled)
|
||||
|
||||
/** Its own flow, as [worldClockSeconds] is (#29). */
|
||||
val compactHomeClock: Flow<Boolean> = store.flow(ClockPrefs.compactHomeClock)
|
||||
|
||||
suspend fun setCompactHomeClock(enabled: Boolean) = store.set(ClockPrefs.compactHomeClock, enabled)
|
||||
|
||||
// --- ring screen over other apps ---
|
||||
|
||||
val ringOverOtherApps: Flow<Boolean> = store.flow(ClockPrefs.ringOverOtherApps)
|
||||
|
||||
@@ -65,6 +65,8 @@ data class BackupSettings(
|
||||
val timerPresets: List<Duration>,
|
||||
/** The world clock's "Display time with seconds" (#23). */
|
||||
val worldClockSeconds: Boolean,
|
||||
/** The world clock's "Compact home clock" (#29). */
|
||||
val compactHomeClock: Boolean = false,
|
||||
val screenSaver: ScreenSaverSettings,
|
||||
/** What the quick-timer Quick Settings tile starts (#26). */
|
||||
val quickTileDuration: Duration = QuickTimerWidgetSettings.DEFAULT,
|
||||
|
||||
@@ -14,8 +14,8 @@ import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
|
||||
/**
|
||||
* The clock tab's settings: the home zone, whether the tab shows seconds and
|
||||
* the automatic local clock. Then the screen saver's: it is a clock face, and
|
||||
* The clock tab's settings: the home zone, whether home is a face or a row,
|
||||
* whether the tab shows seconds and the automatic local clock. Then the screen saver's: it is a clock face, and
|
||||
* the settings Android's own screen saver screen opens are the same rows.
|
||||
* The zone picker itself is ViewModel state, drawn by the hub.
|
||||
*/
|
||||
@@ -35,6 +35,18 @@ internal fun ClockSettingsScreen(
|
||||
position = Position.Top,
|
||||
onClick = viewModel::onOpenHomeZonePicker,
|
||||
)
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_compact_home_clock),
|
||||
summary = stringResource(R.string.settings_compact_home_clock_hint),
|
||||
position = Position.Middle,
|
||||
trailing = {
|
||||
Switch(
|
||||
checked = state.compactHomeClock,
|
||||
onCheckedChange = viewModel::onCompactHomeClockChanged,
|
||||
)
|
||||
},
|
||||
onClick = { viewModel.onCompactHomeClockChanged(!state.compactHomeClock) },
|
||||
)
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_world_clock_seconds),
|
||||
summary = stringResource(R.string.settings_world_clock_seconds_hint),
|
||||
|
||||
@@ -16,6 +16,8 @@ internal sealed interface SettingsUiState {
|
||||
val selfCheckProblemCount: Int? = null,
|
||||
/** The world clock's "Display time with seconds" (#23); not a clock default. */
|
||||
val worldClockSeconds: Boolean,
|
||||
/** The world clock's "Compact home clock" (#29); not a clock default either. */
|
||||
val compactHomeClock: Boolean,
|
||||
/** The stored choice; the screen pairs it with the overlay permission. */
|
||||
val ringOverOtherApps: Boolean,
|
||||
) : SettingsUiState
|
||||
|
||||
@@ -72,10 +72,18 @@ class SettingsViewModel @Inject internal constructor(
|
||||
settings.appearance,
|
||||
settings.defaults,
|
||||
selfCheckProblemCount,
|
||||
settings.worldClockSeconds,
|
||||
// Paired, since `combine` stops at five typed flows.
|
||||
combine(settings.worldClockSeconds, settings.compactHomeClock, ::Pair),
|
||||
settings.ringOverOtherApps,
|
||||
) { appearance, defaults, problemCount, worldClockSeconds, ringOverOtherApps ->
|
||||
SettingsUiState.Loaded(appearance, defaults, problemCount, worldClockSeconds, ringOverOtherApps)
|
||||
) { appearance, defaults, problemCount, (worldClockSeconds, compactHomeClock), ringOverOtherApps ->
|
||||
SettingsUiState.Loaded(
|
||||
appearance = appearance,
|
||||
defaults = defaults,
|
||||
selfCheckProblemCount = problemCount,
|
||||
worldClockSeconds = worldClockSeconds,
|
||||
compactHomeClock = compactHomeClock,
|
||||
ringOverOtherApps = ringOverOtherApps,
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(SUBSCRIPTION_GRACE_MS), SettingsUiState.Loading)
|
||||
|
||||
/**
|
||||
@@ -254,6 +262,9 @@ class SettingsViewModel @Inject internal constructor(
|
||||
fun onWorldClockSecondsChanged(enabled: Boolean) =
|
||||
viewModelScope.launch { settings.setWorldClockSeconds(enabled) }
|
||||
|
||||
fun onCompactHomeClockChanged(enabled: Boolean) =
|
||||
viewModelScope.launch { settings.setCompactHomeClock(enabled) }
|
||||
|
||||
fun onRingOverOtherAppsChanged(enabled: Boolean) =
|
||||
viewModelScope.launch { settings.setRingOverOtherApps(enabled) }
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.DragHandle
|
||||
import androidx.compose.material.icons.rounded.Home
|
||||
import androidx.compose.material.icons.rounded.NearMe
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -22,6 +23,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
@@ -227,16 +229,70 @@ internal fun LocalClockRow(
|
||||
) {
|
||||
// "Local time · 7 hours ahead": the label is what tells it apart from a
|
||||
// saved city, so it leads the summary rather than hiding in a glyph.
|
||||
val summary = stringResource(R.string.world_clock_summary, stringResource(R.string.world_clock_local), summaryFor(row))
|
||||
val time = row.time?.let(formatTime)
|
||||
// `LocalClock` only offers a zone the device lists, so the time is there;
|
||||
// the fallback keeps TalkBack from reading an empty slot if that changes.
|
||||
// Spoken to the minute, as a saved row is.
|
||||
val spokenTime = row.time?.let(formatSpokenTime)
|
||||
// the pinned row's fallback keeps TalkBack from reading an empty slot if
|
||||
// that changes.
|
||||
PinnedClockRow(
|
||||
icon = Icons.Rounded.NearMe,
|
||||
name = row.name,
|
||||
summary = stringResource(R.string.world_clock_summary, stringResource(R.string.world_clock_local), summaryFor(row)),
|
||||
time = row.time,
|
||||
formatTime = formatTime,
|
||||
formatSpokenTime = formatSpokenTime,
|
||||
timeStyle = timeStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Home as a row, for the compact home clock (#29): drawn as the local row is,
|
||||
* with a home glyph, and with home's long zone name in the summary where the
|
||||
* face would caption it. Home is not stored in the list either, so it has
|
||||
* nothing to move or remove.
|
||||
*/
|
||||
@Composable
|
||||
internal fun HomeClockRow(
|
||||
home: HomeFaceState,
|
||||
formatTime: (LocalTime) -> String,
|
||||
formatSpokenTime: (LocalTime) -> String,
|
||||
timeStyle: RowTimeStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val homeLabel = stringResource(R.string.world_clock_home)
|
||||
PinnedClockRow(
|
||||
icon = Icons.Rounded.Home,
|
||||
name = home.city,
|
||||
summary = home.zoneName?.let { stringResource(R.string.world_clock_summary, homeLabel, it) } ?: homeLabel,
|
||||
time = home.time,
|
||||
formatTime = formatTime,
|
||||
formatSpokenTime = formatSpokenTime,
|
||||
timeStyle = timeStyle,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A row outside the reorderable column: a glyph where the drag handle would
|
||||
* be, no actions, and a summary allowed two lines — a label is prepended to
|
||||
* it, and on a phone the day ("Tomorrow") or the zone name is what an
|
||||
* ellipsis would otherwise eat. Spoken to the minute, as a saved row is.
|
||||
*/
|
||||
@Composable
|
||||
private fun PinnedClockRow(
|
||||
icon: ImageVector,
|
||||
name: String,
|
||||
summary: String,
|
||||
time: LocalTime?,
|
||||
formatTime: (LocalTime) -> String,
|
||||
formatSpokenTime: (LocalTime) -> String,
|
||||
timeStyle: RowTimeStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val spokenTime = time?.let(formatSpokenTime)
|
||||
val description = if (spokenTime != null) {
|
||||
stringResource(R.string.world_clock_row_description, row.name, spokenTime, summary)
|
||||
stringResource(R.string.world_clock_row_description, name, spokenTime, summary)
|
||||
} else {
|
||||
stringResource(R.string.world_clock_row_description_unknown, row.name, summary)
|
||||
stringResource(R.string.world_clock_row_description_unknown, name, summary)
|
||||
}
|
||||
|
||||
GroupedSurface(
|
||||
@@ -253,19 +309,17 @@ internal fun LocalClockRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.NearMe,
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
// Two lines, not one: "Local time · " is prepended, and on a phone
|
||||
// the day ("Tomorrow") is what an ellipsis would otherwise eat.
|
||||
RowText(name = row.name, summary = summary, time = time, timeStyle = timeStyle, summaryMaxLines = 2)
|
||||
RowText(name = name, summary = summary, time = time?.let(formatTime), timeStyle = timeStyle, summaryMaxLines = 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The name over the summary, then the time: what a saved row and the local row share. */
|
||||
/** The name over the summary, then the time: what a saved row and a pinned row share. */
|
||||
@Composable
|
||||
private fun RowScope.RowText(
|
||||
name: String,
|
||||
|
||||
@@ -59,6 +59,8 @@ internal sealed interface WorldClockUiState {
|
||||
* `WorldClocks.MAX`.
|
||||
*/
|
||||
val local: WorldClockRowState? = null,
|
||||
/** Home drawn as a row like the cities' rather than the analog face (#29). */
|
||||
val compactHome: Boolean = false,
|
||||
) : WorldClockUiState
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ import java.time.LocalTime
|
||||
* digits cannot: day or night at home, at a glance. The caption is where the
|
||||
* ICU long zone name is shown, which is the one place it earns its length.
|
||||
*
|
||||
* The compact home clock (#29) trades the face for a row like the cities',
|
||||
* for whoever wants the tab dense and digital throughout. The automatic local
|
||||
* clock then sits under that row exactly as it sits under the face.
|
||||
*
|
||||
* `scrollable` stays true: `ReorderableColumn` is not lazy and cannot live
|
||||
* inside a `LazyColumn`, and a list capped at twenty-four rows is exactly the
|
||||
* shape it was written for (D12).
|
||||
@@ -122,12 +126,23 @@ fun WorldClockScreen(
|
||||
},
|
||||
) {
|
||||
if (loaded != null) {
|
||||
HomeFace(
|
||||
home = loaded.home,
|
||||
showSeconds = loaded.showSeconds,
|
||||
formatTime = formatTime,
|
||||
formatSpokenTime = formatSpokenTime,
|
||||
)
|
||||
if (loaded.compactHome) {
|
||||
HomeClockRow(
|
||||
home = loaded.home,
|
||||
formatTime = formatTime,
|
||||
formatSpokenTime = formatSpokenTime,
|
||||
timeStyle = rowTimeStyle,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
} else {
|
||||
HomeFace(
|
||||
home = loaded.home,
|
||||
showSeconds = loaded.showSeconds,
|
||||
formatTime = formatTime,
|
||||
formatSpokenTime = formatSpokenTime,
|
||||
)
|
||||
}
|
||||
|
||||
// Above the saved cities and outside their column: it is not one
|
||||
// of them, and a drag must not be able to land between the two.
|
||||
|
||||
@@ -82,6 +82,9 @@ internal class WorldClockSource @Inject constructor(
|
||||
ticker.alignedTicks(WorldClockDefaults.tickFor(seconds))
|
||||
.map { build(clocks, storedHome, localClock, seconds) }
|
||||
}
|
||||
// Outside the tick: how home is drawn changes nothing it computes,
|
||||
// so flipping it neither restarts the ticker nor waits for it.
|
||||
.combine(settings.compactHomeClock) { loaded, compact -> loaded.copy(compactHome = compact) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
/**
|
||||
|
||||
@@ -357,9 +357,11 @@
|
||||
<string name="settings_timer_defaults_hint">How timers ring. A timer can still pick its own sound in its editor.</string>
|
||||
<string name="settings_timer_vibrate_off">Off. A timer set to Silent still vibrates</string>
|
||||
<string name="settings_section_clock">Clock</string>
|
||||
<string name="settings_clock_subtitle">Home time zone, seconds, local clock and screen saver</string>
|
||||
<string name="settings_clock_subtitle">Home time zone and clock, seconds, local clock and screen saver</string>
|
||||
<string name="settings_clock_hint">World clocks show how far each city is from your home time zone.</string>
|
||||
<string name="settings_home_zone">Home time zone</string>
|
||||
<string name="settings_compact_home_clock">Compact home clock</string>
|
||||
<string name="settings_compact_home_clock_hint">Show home as a row like the other cities, without the analog clock.</string>
|
||||
<string name="settings_world_clock_seconds">Display time with seconds</string>
|
||||
<string name="settings_world_clock_seconds_hint">The world clock shows seconds and a second hand. Uses a little more battery.</string>
|
||||
<string name="settings_automatic_local_clock">Automatic local clock</string>
|
||||
|
||||
@@ -44,6 +44,7 @@ class BackupEngineTest {
|
||||
source.settings.setSnoozeMinutes(7)
|
||||
source.settings.setHomeZoneId("America/New_York")
|
||||
source.settings.setWorldClockSeconds(true)
|
||||
source.settings.setCompactHomeClock(true)
|
||||
|
||||
val exported = source.backupEngine.export()
|
||||
|
||||
@@ -70,6 +71,7 @@ class BackupEngineTest {
|
||||
assertThat(target.settings.currentDefaults().snoozeMinutes).isEqualTo(7)
|
||||
assertThat(target.settings.currentDefaults().homeZoneId).isEqualTo("America/New_York")
|
||||
assertThat(target.settings.worldClockSeconds.first()).isTrue()
|
||||
assertThat(target.settings.compactHomeClock.first()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -79,6 +79,7 @@ class BackupCodecTest {
|
||||
),
|
||||
timerPresets = listOf(1.minutes, 2.minutes, 10.minutes),
|
||||
worldClockSeconds = true,
|
||||
compactHomeClock = true,
|
||||
quickTileDuration = 3.minutes,
|
||||
screenSaver = ScreenSaverSettings(style = ScreenSaverStyle.ANALOG, nightMode = true),
|
||||
bedtime = BedtimeSettings(
|
||||
@@ -483,6 +484,17 @@ class BackupCodecTest {
|
||||
assertThat(BackupCodec.encode(sampleContent())).contains("\"world_clock_show_seconds\": true")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a file from before the compact home clock keeps it off`() {
|
||||
val settings = (BackupCodec.decode(minimalDocument()) as BackupResult.Success).content.settings
|
||||
assertThat(settings.compactHomeClock).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the compact home clock is written under its preference key`() {
|
||||
assertThat(BackupCodec.encode(sampleContent())).contains("\"world_clock_compact_home\": true")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a file from before the automatic local clock keeps it on`() {
|
||||
val settings = (BackupCodec.decode(minimalDocument()) as BackupResult.Success).content.settings
|
||||
|
||||
@@ -115,6 +115,25 @@ class ClockPrefsTest {
|
||||
assertThat(prefs.worldClockSeconds.first()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the compact home clock round-trips without touching the clock defaults`(@TempDir tempDir: Path) = runTest {
|
||||
val prefs = SettingsPrefs(PrefStore(newDataStore(tempDir)))
|
||||
assertThat(prefs.compactHomeClock.first()).isFalse()
|
||||
|
||||
prefs.defaults.test {
|
||||
assertThat(awaitItem()).isEqualTo(ClockDefaults())
|
||||
|
||||
prefs.setCompactHomeClock(true)
|
||||
// A barrier on this same flow, as for show-seconds above.
|
||||
prefs.setSnoozeMinutes(7)
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(ClockDefaults(snoozeMinutes = 7))
|
||||
expectNoEvents()
|
||||
cancel()
|
||||
}
|
||||
assertThat(prefs.compactHomeClock.first()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `snooze minutes clamp on write`(@TempDir tempDir: Path) = runTest {
|
||||
val prefs = SettingsPrefs(PrefStore(newDataStore(tempDir)))
|
||||
|
||||
@@ -106,6 +106,7 @@ class SettingsPrefsTest {
|
||||
),
|
||||
timerPresets = listOf(5.minutes, 1.minutes, 5.minutes),
|
||||
worldClockSeconds = false,
|
||||
compactHomeClock = true,
|
||||
quickTileDuration = 2.minutes,
|
||||
screenSaver = ScreenSaverSettings(ScreenSaverStyle.ANALOG, nightMode = true),
|
||||
),
|
||||
@@ -122,6 +123,7 @@ class SettingsPrefsTest {
|
||||
assertThat(prefs.currentDefaults().timerVibrate).isFalse()
|
||||
assertThat(prefs.currentDefaults().timerVolumeRampSeconds).isEqualTo(60)
|
||||
assertThat(prefs.currentDefaults().automaticLocalClock).isFalse()
|
||||
assertThat(prefs.compactHomeClock.first()).isTrue()
|
||||
assertThat(prefs.timerPresets.first()).containsExactly(1.minutes, 5.minutes).inOrder()
|
||||
assertThat(prefs.quickTileDuration.first()).isEqualTo(2.minutes)
|
||||
assertThat(prefs.screenSaver.first()).isEqualTo(ScreenSaverSettings(ScreenSaverStyle.ANALOG, nightMode = true))
|
||||
|
||||
@@ -235,6 +235,19 @@ class SettingsViewModelTest {
|
||||
assertThat((viewModel.state.value as SettingsUiState.Loaded).worldClockSeconds).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the compact home clock switch writes through`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val viewModel = collecting(newViewModel(tempDir))
|
||||
advanceUntilIdle()
|
||||
assertThat((viewModel.state.value as SettingsUiState.Loaded).compactHomeClock).isFalse()
|
||||
|
||||
viewModel.onCompactHomeClockChanged(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat((viewModel.state.value as SettingsUiState.Loaded).compactHomeClock).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `choosing a default alarm ringtone writes through and closes the picker`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
|
||||
@@ -420,6 +420,21 @@ class WorldClockViewModelTest {
|
||||
assertThat(harness.ticker.requestedPeriods).isEmpty()
|
||||
}
|
||||
|
||||
/** #29: how home is drawn is not something the tab ticks for. */
|
||||
@Test
|
||||
fun `the compact home clock reaches the tab without restarting the ticker`(@TempDir tempDir: Path) =
|
||||
runTest(main.dispatcher) {
|
||||
val harness = collecting(worldClockHarness(tempDir))
|
||||
advanceUntilIdle()
|
||||
assertThat(harness.loaded().compactHome).isFalse()
|
||||
|
||||
harness.settings.setCompactHomeClock(true)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(harness.loaded().compactHome).isTrue()
|
||||
assertThat(harness.ticker.requestedUnits).containsExactly(WorldClockDefaults.tickFor(showSeconds = false))
|
||||
}
|
||||
|
||||
/** #23: the tab wakes every second only while the user asked for seconds. */
|
||||
@Test
|
||||
fun `turning seconds on and off moves the cadence with it`(@TempDir tempDir: Path) =
|
||||
|
||||
@@ -618,7 +618,7 @@ Six slices:
|
||||
|---|---|---|
|
||||
| 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_timer_vibrate`, `default_timer_volume_ramp_seconds` (#22), `default_dismiss_challenge`, `home_zone_id`, `volume_buttons_while_ringing` (#18; app-wide, no per-alarm override), `automatic_local_clock` (#24) | `ClockPrefs`, surfaced as `SettingsPrefs.defaults: Flow<ClockDefaults>` |
|
||||
| World clock display (#23) | `world_clock_show_seconds` | `ClockPrefs`, surfaced as its own `SettingsPrefs.worldClockSeconds`, not a clock default, so flipping it re-emits nothing alarms and timers watch |
|
||||
| World clock display (#23, #29) | `world_clock_show_seconds`, `world_clock_compact_home` | `ClockPrefs`, surfaced as their own `SettingsPrefs.worldClockSeconds` and `SettingsPrefs.compactHomeClock`, not clock defaults, so flipping them re-emits nothing alarms and timers watch |
|
||||
| Stopwatch run record (M2) | `stopwatch_state`, `stopwatch_started_elapsed_millis`, `stopwatch_accumulated_millis`, `stopwatch_last_lap_cumulative_millis` (M7 is the first caller of the last, 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` |
|
||||
|
||||
+3
-1
@@ -164,6 +164,7 @@ device or after a reboot (`docs/PLAN.md` §5). Every imported timer lands
|
||||
"default_timer_silence_after_minutes": 10,
|
||||
"volume_buttons_while_ringing": "CONTROL_VOLUME",
|
||||
"world_clock_show_seconds": false,
|
||||
"world_clock_compact_home": false,
|
||||
"automatic_local_clock": true,
|
||||
"timer_presets": [60000, 120000, 180000, 300000, 600000, 900000, 1800000, 3600000],
|
||||
"bedtime": {
|
||||
@@ -213,7 +214,8 @@ rings for that long anyway); a negative one reads as the default, 10.
|
||||
|
||||
Files from before `world_clock_show_seconds` existed import with it off, its
|
||||
default. It travels with the settings but is not a clock default: nothing an
|
||||
alarm or timer inherits depends on it. A file without `automatic_local_clock`
|
||||
alarm or timer inherits depends on it. The same goes for
|
||||
`world_clock_compact_home` (#29), which a file without it restores as off. A file without `automatic_local_clock`
|
||||
(one written before it existed) restores it as `true`, its default.
|
||||
|
||||
Every numeric/duration value is clamped on *apply* using the same ranges and encoding
|
||||
|
||||
Reference in new issue
Block a user