diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt b/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt index 18aba23..759723b 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/backup/BackupEngine.kt @@ -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(), diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt index f4231de..a3ce2fa 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupCodec.kt @@ -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, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt index 1e6a3e4..4893392 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/backup/BackupDto.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt index 452ac96..acb5467 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefs.kt @@ -74,6 +74,13 @@ object ClockPrefs { */ val worldClockSeconds: Pref = 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 = 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 diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt index 755c879..6112d86 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefs.kt @@ -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 = store.flow(ClockPrefs.compactHomeClock) + + suspend fun setCompactHomeClock(enabled: Boolean) = store.set(ClockPrefs.compactHomeClock, enabled) + // --- ring screen over other apps --- val ringOverOtherApps: Flow = store.flow(ClockPrefs.ringOverOtherApps) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt index 37a8f73..b4394a4 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/backup/BackupContent.kt @@ -65,6 +65,8 @@ data class BackupSettings( val timerPresets: List, /** 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, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/ClockSettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/ClockSettingsScreen.kt index d23bb78..cb7adf4 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/ClockSettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/ClockSettingsScreen.kt @@ -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), diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt index 7cb1cbc..a67c5cf 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsUiState.kt @@ -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 diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt index 6d53dde..368e614 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModel.kt @@ -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) } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt index 3fbae81..01f42d1 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRow.kt @@ -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, diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt index 1ad1d3b..101b63a 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockRowState.kt @@ -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 } diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt index 77703f2..4d3543c 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockScreen.kt @@ -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. diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt index a3eaeb3..3306d0e 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockSource.kt @@ -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() /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3d8960..221cfcc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,9 +357,11 @@ How timers ring. A timer can still pick its own sound in its editor. Off. A timer set to Silent still vibrates Clock - Home time zone, seconds, local clock and screen saver + Home time zone and clock, seconds, local clock and screen saver World clocks show how far each city is from your home time zone. Home time zone + Compact home clock + Show home as a row like the other cities, without the analog clock. Display time with seconds The world clock shows seconds and a second hand. Uses a little more battery. Automatic local clock diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/backup/BackupEngineTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/backup/BackupEngineTest.kt index 8a88a8f..7e56265 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/backup/BackupEngineTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/backup/BackupEngineTest.kt @@ -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 diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt index 9e90e31..ee7d18e 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/backup/BackupCodecTest.kt @@ -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 diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt index fba9dc0..b95faab 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/ClockPrefsTest.kt @@ -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))) diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt index 872a1c4..8a42f5b 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/data/prefs/SettingsPrefsTest.kt @@ -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)) diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt index 8e951ce..e36f005 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/settings/SettingsViewModelTest.kt @@ -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) { diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockViewModelTest.kt index 3cf716d..d753b05 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockViewModelTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/ui/worldclock/WorldClockViewModelTest.kt @@ -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) = diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2d55a85..b7c7d89 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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` | -| 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` | diff --git a/docs/BACKUP.md b/docs/BACKUP.md index 6268ede..420c59d 100644 --- a/docs/BACKUP.md +++ b/docs/BACKUP.md @@ -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