From f46b7e1e370e50357c6e5025870b3000ea86176a Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 21 Oct 2026 20:24:24 +0200 Subject: [PATCH] fix(worldclock): keep the local row's day label and legacy city names The local row prepends "Local time" to the offset and day, so on a phone a one-line summary cut off the day ("Tomorrow"), the part that matters most on a long trip. It now wraps to two lines. Only Etc/ offsets and bare abbreviations are named by their offset; a legacy place id such as US/Pacific keeps its place name. The picker's home selection now derives from the same settings subscription the tab uses. --- .../clockula/domain/worldclock/ZoneCatalog.kt | 11 +++++++---- .../clockula/ui/worldclock/WorldClockRow.kt | 7 +++++-- .../clockula/ui/worldclock/WorldClockRowState.kt | 2 +- .../clockula/ui/worldclock/WorldClockSource.kt | 8 ++++---- .../clockula/domain/worldclock/LocalClockTest.kt | 5 ++--- .../clockula/domain/worldclock/ZoneCatalogTest.kt | 11 ++++++----- docs/ARCHITECTURE.md | 2 +- 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalog.kt b/app/src/main/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalog.kt index a66adac..b72ad6a 100644 --- a/app/src/main/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalog.kt +++ b/app/src/main/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalog.kt @@ -69,11 +69,14 @@ object ZoneCatalog { entries.sortedWith(compareBy({ ZoneSearch.fold(it.city) }, { it.zoneId })) /** - * True for an id named after a place (`Asia/Tokyo`) or for [UTC]; false for - * the offset and abbreviation ids tzdata also lists (`Etc/GMT-9`, `CET`), - * whose last segment is no city and, for `Etc/GMT±n`, has the sign reversed. + * True when [cityFallbackFor] gives a fair name for [zoneId]: a place id, + * current (`Asia/Tokyo`) or legacy (`US/Pacific`), or [UTC]. False for the + * `Etc/` offsets and the bare abbreviations tzdata also lists (`Etc/GMT-9`, + * `CET`), whose last segment is no place and, for `Etc/GMT±n`, has the sign + * reversed. */ - fun isCityId(zoneId: String): Boolean = zoneId == UTC || isRegionId(zoneId) + fun hasPlaceName(zoneId: String): Boolean = + zoneId == UTC || ('/' in zoneId && !zoneId.startsWith("Etc/")) private fun isRegionId(zoneId: String): Boolean = zoneId.contains('/') && zoneId.substringBefore('/') in REGIONS 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 4282acf..31a25ef 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 @@ -175,7 +175,9 @@ internal fun LocalClockRow( tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(24.dp), ) - RowText(name = row.name, summary = summary, time = time) + // 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, summaryMaxLines = 2) } } } @@ -187,6 +189,7 @@ private fun RowScope.RowText( summary: String, time: String?, timeColor: Color = MaterialTheme.colorScheme.onSurface, + summaryMaxLines: Int = 1, ) { Column(modifier = Modifier.weight(1f)) { Text( @@ -200,7 +203,7 @@ private fun RowScope.RowText( text = summary, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, + maxLines = summaryMaxLines, overflow = TextOverflow.Ellipsis, ) } 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 eae5540..0bded4d 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 @@ -113,7 +113,7 @@ internal object WorldClockRows { entries: Map, zoneNames: Map, ): WorldClockRowState { - val name = if (ZoneCatalog.isCityId(deviceZone.id)) { + val name = if (ZoneCatalog.hasPlaceName(deviceZone.id)) { null } else { ZoneOffsetFormat.gmtLabel(ZoneComparisons.utcOffsetMinutes(deviceZone, now)) 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 bd54447..d7f655f 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 @@ -35,13 +35,13 @@ internal class WorldClockSource @Inject constructor( /** What the picker needs before it can draw a row; null while it is still being built. */ private data class Catalog(val entries: List, val deviceCity: String) - private val storedHomeZoneId: Flow = - settings.defaults.map { it.homeZoneId }.distinctUntilChanged() - - /** The two settings the tab reads, from one subscription to the defaults. */ + /** The two settings the tab reads: the stored home zone and the local-clock switch. */ private val tabSettings: Flow> = settings.defaults.map { it.homeZoneId to it.automaticLocalClock }.distinctUntilChanged() + private val storedHomeZoneId: Flow = + tabSettings.map { it.first }.distinctUntilChanged() + /** * The tab's state. Both the instant and the device zone are re-read on * **every tick**, which is what makes `TIME_SET` and `TIMEZONE_CHANGED` diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/LocalClockTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/LocalClockTest.kt index d980afb..86ad5e3 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/LocalClockTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/LocalClockTest.kt @@ -48,9 +48,8 @@ class LocalClockTest { } @Test - fun `the offset is the one at the instant, so DST counts`() { - // London is an hour behind Berlin all year, but Lisbon and London agree - // in summer and in winter: the rule is the offset now, not the zone id. + fun `the offset decides, not the zone id`() { + // Lisbon and London are different ids on the same offset; Berlin is not. assertThat(show(storedHome = "Europe/Lisbon", device = london)).isNull() assertThat(show(storedHome = "Europe/Berlin", device = london)).isEqualTo(london) } diff --git a/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalogTest.kt b/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalogTest.kt index f18cb5b..4843e01 100644 --- a/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalogTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/clockula/domain/worldclock/ZoneCatalogTest.kt @@ -226,10 +226,11 @@ class ZoneCatalogTest { } @Test - fun `a city id is a region id or UTC, never an offset or abbreviation`() { - assertThat(ZoneCatalog.isCityId("Asia/Tokyo")).isTrue() - assertThat(ZoneCatalog.isCityId("UTC")).isTrue() - assertThat(ZoneCatalog.isCityId("Etc/GMT-9")).isFalse() - assertThat(ZoneCatalog.isCityId("CET")).isFalse() + fun `a place id or UTC has a place name, an offset or abbreviation does not`() { + assertThat(ZoneCatalog.hasPlaceName("Asia/Tokyo")).isTrue() + assertThat(ZoneCatalog.hasPlaceName("US/Pacific")).isTrue() + assertThat(ZoneCatalog.hasPlaceName("UTC")).isTrue() + assertThat(ZoneCatalog.hasPlaceName("Etc/GMT-9")).isFalse() + assertThat(ZoneCatalog.hasPlaceName("CET")).isFalse() } } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 58f296c..23fedc9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2407,7 +2407,7 @@ differ from it, so with no stored home the rule is always null, and Settings › Clock says so under the switch rather than hiding it. A device set by UTC offset rather than by city reports an id like `Etc/GMT-9`, whose last segment has its sign reversed, so that row is named by its real offset ("GMT+9") -instead (`ZoneCatalog.isCityId`). +instead (`ZoneCatalog.hasPlaceName`). The row is not stored. It is `WorldClockUiState.Loaded.local`, built per tick by the source from the same `zones.current()` read the face uses, so a