Draw the weather instead of spelling it

400c82c · Gabriel Morell · 2026-09-08 21:06

34 files +277 -22
Message
{commit_body(@commit)}

Files changed

modified clients/android/app/src/main/java/io/neiam/apolloscrib/ui/RichCard.kt
+19 −9
@@ -287,16 +287,26 @@ fun RichFactsCard(facts: RichFacts, modifier: Modifier = Modifier) {
287 287 }
288 288
289 289 if (facts.headline != null) {
290 − Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
291 − Text(facts.headline, style = MaterialTheme.typography.displaySmall)
292 −
293 − facts.caption?.let {
294 − Text(
295 − it,
296 − style = MaterialTheme.typography.titleSmall,
297 − color = palette.dim
298 − )
290 + Row(verticalAlignment = Alignment.CenterVertically) {
291 + Column(
292 + Modifier.weight(1f),
293 + verticalArrangement = Arrangement.spacedBy(2.dp)
294 + ) {
295 + Text(facts.headline, style = MaterialTheme.typography.displaySmall)
296 +
297 + facts.caption?.let {
298 + Text(
299 + it,
300 + style = MaterialTheme.typography.titleSmall,
301 + color = palette.dim
302 + )
303 + }
299 304 }
305 +
306 + // The picture of the answer, opposite the number. Sized to
307 + // the headline rather than to the text beside it: from
308 + // across a room this is the part that reads.
309 + facts.glyph?.let { glyph -> Glyph(glyph, 52.dp, palette.accent) }
300 310 }
301 311 } else {
302 312 facts.caption?.let {
modified clients/android/app/src/main/java/io/neiam/apolloscrib/ui/RichFacts.kt
+51 −1
@@ -30,7 +30,16 @@ data class RichFacts(
30 30 val iconRes: Int,
31 31 val headline: String? = null,
32 32 val caption: String? = null,
33 − val facts: List<Fact> = emptyList()
33 + val facts: List<Fact> = emptyList(),
34 + /**
35 + * A picture of the answer, drawn large beside the headline.
36 + *
37 + * Only where there is one worth drawing. "Clouds" is a word; a cloud is
38 + * the same fact read without reading, which is what a card glanced at
39 + * from across a room needs. Null everywhere the answer is a number and a
40 + * picture would only be decoration.
41 + */
42 + val glyph: Int? = null
34 43 ) {
35 44 data class Fact(val label: String, val value: String)
36 45 }
@@ -60,6 +69,7 @@ private fun weather(entry: VisionEntry): RichFacts? {
60 69 iconRes = R.drawable.fa_temperature_half,
61 70 headline = now.temp?.let { "${it.roundToInt()}$degrees" },
62 71 caption = now.weather,
72 + glyph = skyGlyph(now.weather),
63 73 facts = listOfNotNull(
64 74 now.feel?.let { RichFacts.Fact("Feels like", "${it.roundToInt()}$degrees") },
65 75 now.hum?.let { RichFacts.Fact("Humidity", "${it.roundToInt()}%") },
@@ -182,6 +192,46 @@ private fun gbfs(entry: VisionEntry): RichFacts? {
182 192 )
183 193 }
184 194
195 +/**
196 + * What the sky is doing, as a picture.
197 + *
198 + * OpenWeather's condition groups, which is what the publisher sends through
199 + * untouched. Matched loosely on purpose: agencies and forecasts spell these
200 + * inconsistently, and a card showing a cloud for something drizzle-shaped is
201 + * right enough, where showing nothing at all is a hole in the card.
202 + *
203 + * Null for anything unrecognised rather than a guess. A wrong picture is read
204 + * before the word beside it and believed instead of it.
205 + */
206 +private fun skyGlyph(condition: String?): Int? = when {
207 + condition == null -> null
208 + condition.contains("thunder", true) || condition.contains("storm", true) ->
209 + R.drawable.fa_cloud_bolt
210 + condition.contains("drizzle", true) -> R.drawable.fa_cloud_rain
211 + condition.contains("rain", true) || condition.contains("shower", true) ->
212 + R.drawable.fa_cloud_showers_heavy
213 + condition.contains("snow", true) || condition.contains("sleet", true) ->
214 + R.drawable.fa_snowflake
215 + condition.contains("tornado", true) || condition.contains("squall", true) ->
216 + R.drawable.fa_tornado
217 + // Mist, fog, haze, smoke, dust, sand, ash: all the ways air stops being
218 + // clear, and one glyph for the lot of them.
219 + condition.contains("mist", true) || condition.contains("fog", true) ||
220 + condition.contains("haze", true) || condition.contains("smoke", true) ||
221 + condition.contains("dust", true) || condition.contains("sand", true) ||
222 + condition.contains("ash", true) -> R.drawable.fa_smog
223 + condition.contains("clear", true) || condition.contains("sun", true) ->
224 + R.drawable.fa_sun
225 + // "Few clouds" and "scattered clouds" are not the same sky as "overcast",
226 + // and the sun showing through says which.
227 + condition.contains("few", true) || condition.contains("scattered", true) ||
228 + condition.contains("part", true) -> R.drawable.fa_cloud_sun
229 + condition.contains("cloud", true) || condition.contains("overcast", true) ->
230 + R.drawable.fa_cloud
231 + condition.contains("wind", true) -> R.drawable.fa_wind
232 + else -> null
233 +}
234 +
185 235 /**
186 236 * Whatever the compact renderer made of it, laid out with room.
187 237 *
modified clients/android/app/src/main/java/io/neiam/apolloscrib/widget/RichBoardWidgetService.kt
+9 −0
@@ -85,6 +85,15 @@ class RichBoardWidgetService : RemoteViewsService() {
85 85 if (card.caption == null) android.view.View.GONE else android.view.View.VISIBLE
86 86 )
87 87
88 + // The picture of the answer, where there is one worth drawing.
89 + if (card.glyph != null) {
90 + setImageViewResource(R.id.rich_glyph, card.glyph)
91 + setInt(R.id.rich_glyph, "setColorFilter", palette.accent.toArgb())
92 + setViewVisibility(R.id.rich_glyph, android.view.View.VISIBLE)
93 + } else {
94 + setViewVisibility(R.id.rich_glyph, android.view.View.GONE)
95 + }
96 +
88 97 removeAllViews(R.id.rich_facts)
89 98
90 99 // Four, because a widget card does not scroll and a fifth row
added clients/android/app/src/main/res/drawable/fa_cloud.xml
+12 −0
@@ -0,0 +1,12 @@
1 +<!-- Generated by tools/fa_to_vector.py from Font Awesome Free 6.2.0 (cloud).
2 + Font Awesome Free icons are CC BY 4.0. Do not edit by hand. -->
3 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
4 + android:width="24dp"
5 + android:height="24dp"
6 + android:viewportWidth="640"
7 + android:viewportHeight="512"
8 + android:tint="?android:attr/colorControlNormal">
9 + <path
10 + android:fillColor="@android:color/white"
11 + android:pathData="M0 336c0 79.5 64.5 144 144 144H512c70.7 0 128-57.3 128-128c0-61.9-44-113.6-102.4-125.4c4.1-10.7 6.4-22.4 6.4-34.6c0-53-43-96-96-96c-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32C167.6 32 96 103.6 96 192c0 2.7 .1 5.4 .2 8.1C40.2 219.8 0 273.2 0 336z" />
12 +</vector>
added clients/android/app/src/main/res/drawable/fa_cloud_bolt.xml
+12 −0
@@ -0,0 +1,12 @@
1 +<!-- Generated by tools/fa_to_vector.py from Font Awesome Free 6.2.0 (cloud-bolt).
2 + Font Awesome Free icons are CC BY 4.0. Do not edit by hand. -->
3 +<vector xmlns:android="http://schemas.android.com/apk/res/android"
4 + android:width="24dp"
5 + android:height="24dp"
6 + android:viewportWidth="512"
7 + android:viewportHeight="512"
8 + android:tint="?android:attr/colorControlNormal">
9 + <path
10 + android:fillColor="@android:color/white"
11 + android:pathData="M0 224c0 53 43 96 96 96h47.2L290 202.5c17.6-14.1 42.6-14 60.2 .2s22.8 38.6 12.8 58.8L333.7 320H352h64c53 0 96-43 96-96s-43-96-96-96c-.5 0-1.1 0-1.6 0c1.1-5.2 1.6-10.5 1.6-16c0-44.2-35.8-80-80-80c-24.3 0-46.1 10.9-60.8 28C256.5 24.3 219.1 0 176 0C114.1 0 64 50.1 64 112c0 7.1 .7 14.1 1.9 20.8C27.6 145.4 0 181.5 0 224zm330.1 3.6c-5.8-4.7-14.2-4.7-20.1-.1l-160 128c-5.3 4.2-7.4 11.4-5.1 17.8s8.3 10.7 15.1 10.7h70.1L177.7 488.8c-3.4 6.7-1.6 14.9 4.3 19.6s14.2 4.7 20.1 .1l160-128c5.3-4.2 7.4-11.4 5.1-17.8s-8.3-10.7-15.1-10.7H281.9l52.4-104.8c3.4-6.7 1.6-14.9-4.2-19.6z" />
12 +</vector>
added clients/android/app/src/main/res/drawable/fa_cloud_rain.xml
+12 −0

Click to load diff…

added clients/android/app/src/main/res/drawable/fa_cloud_showers_heavy.xml
+12 −0

Click to load diff…

added clients/android/app/src/main/res/drawable/fa_smog.xml
+12 −0

Click to load diff…

added clients/android/app/src/main/res/drawable/fa_snowflake.xml
+12 −0

Click to load diff…

added clients/android/app/src/main/res/drawable/fa_tornado.xml
+12 −0

Click to load diff…

modified clients/android/app/src/main/res/layout/widget_rich_card.xml
+33 −12

Click to load diff…

added clients/android/app/src/test/java/io/neiam/apolloscrib/ui/SkyGlyphTest.kt
+77 −0

Click to load diff…

modified clients/android/tools/fa_to_vector.py
+4 −0

Click to load diff…

Parents: 3ead170