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.
This commit is contained in:
2026-10-21 20:24:24 +02:00
parent 1333001aa9
commit f46b7e1e37
7 changed files with 26 additions and 20 deletions
@@ -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
@@ -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,
)
}
@@ -113,7 +113,7 @@ internal object WorldClockRows {
entries: Map<String, ZoneEntry>,
zoneNames: Map<String, String?>,
): WorldClockRowState {
val name = if (ZoneCatalog.isCityId(deviceZone.id)) {
val name = if (ZoneCatalog.hasPlaceName(deviceZone.id)) {
null
} else {
ZoneOffsetFormat.gmtLabel(ZoneComparisons.utcOffsetMinutes(deviceZone, now))
@@ -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<ZoneEntry>, val deviceCity: String)
private val storedHomeZoneId: Flow<String?> =
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<Pair<String?, Boolean>> =
settings.defaults.map { it.homeZoneId to it.automaticLocalClock }.distinctUntilChanged()
private val storedHomeZoneId: Flow<String?> =
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`
@@ -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)
}
@@ -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()
}
}
+1 -1
View File
@@ -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