Draw every source type the crate gives a shape to

084c348 · Gabriel Morell · 2026-09-02 20:40

67 files +1315 -37
Message
{commit_body(@commit)}

Files changed

modified clients/android/README.md
+24 −5
@@ -10,14 +10,33 @@ the Pythiae publishes on change and the phone redraws when it does.
10 10
11 11 ## What it shows
12 12
13 +One Target per source type, added once per query you care about:
14 +
13 15 | Target | Source | What it draws |
14 16 | --- | --- | --- |
15 −| Transit | `gtfs` | The next departures from a stop, soonest route first, with a separate target for a severe or stop-specific alert |
16 −| Bikeshare | `gbfs` | Bikes and docks at a station, or loose bikes for an area query |
17 +| Transit | `gtfs` | Next departures from a stop, soonest route first, each time marked as realtime or scheduled; a severe or stop-specific alert gets its own card |
18 +| Bikeshare | `gbfs` | Bikes, e-bikes and docks at a station, or loose bikes for an area query |
17 19 | Weather | `weather` | Current conditions at a focus |
18 −
19 −Each is added once per query: add the Transit target twice and you get two
20 −stops, each choosing its own query when Smartspacer opens the setup screen.
20 +| Tides | `tidal` | High and low water, in the order the day happens rather than the order the feed sends |
21 +| Air quality | `aqi` | Whatever the monitor measured — nothing is assumed present |
22 +| Sun and moon | `ephem` | Sunrise, sunset, moonrise, moonset, and the phase |
23 +| Calendar | `calendar` | What is coming up, soonest first |
24 +| Pollen | `pollen` | Grass, trees and weeds, worst first, in-season only |
25 +| Drought | `drought` | Drought Monitor coverage, worst category with any of the area in it |
26 +| GitLab / GitHub | `gitlab`, `github` | Recent CI jobs and runs, pass or fail as the row's glyph |
27 +| Cronos, Packages, Constant | `cronos`, `packages`, `const` | Whatever the publisher sent: these are passthroughs, with no shape to render against |
28 +
29 +Add the Transit target twice and you get two stops, each choosing its own query
30 +when Smartspacer opens the setup screen.
31 +
32 +### What is not here
33 +
34 +[`apollos-types`](https://github.com/neiam/apollos-types) is the source of truth
35 +for the wire format, and this client covers exactly the types it gives a shape
36 +to. apollos-crib also publishes `icarus`, `mailbox`, `treasury`, `bourse`,
37 +`hass` and `rideshare`, which the crate does not describe — so this cannot claim
38 +to read them. They are named at the foot of the board rather than dropped in
39 +silence. Adding one means adding it to the crate first.
21 40
22 41 ## Setting it up
23 42
modified clients/android/app/src/main/AndroidManifest.xml
+110 −0
@@ -92,5 +92,115 @@
92 92 </intent-filter>
93 93 </provider>
94 94
95 + <provider
96 + android:name=".targets.TidalTargetProvider"
97 + android:authorities="${applicationId}.target.tidal"
98 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
99 + android:exported="true">
100 + <intent-filter>
101 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
102 + </intent-filter>
103 + </provider>
104 +
105 + <provider
106 + android:name=".targets.AqiTargetProvider"
107 + android:authorities="${applicationId}.target.aqi"
108 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
109 + android:exported="true">
110 + <intent-filter>
111 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
112 + </intent-filter>
113 + </provider>
114 +
115 + <provider
116 + android:name=".targets.EphemTargetProvider"
117 + android:authorities="${applicationId}.target.ephem"
118 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
119 + android:exported="true">
120 + <intent-filter>
121 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
122 + </intent-filter>
123 + </provider>
124 +
125 + <provider
126 + android:name=".targets.CalendarTargetProvider"
127 + android:authorities="${applicationId}.target.calendar"
128 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
129 + android:exported="true">
130 + <intent-filter>
131 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
132 + </intent-filter>
133 + </provider>
134 +
135 + <provider
136 + android:name=".targets.PollenTargetProvider"
137 + android:authorities="${applicationId}.target.pollen"
138 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
139 + android:exported="true">
140 + <intent-filter>
141 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
142 + </intent-filter>
143 + </provider>
144 +
145 + <provider
146 + android:name=".targets.DroughtTargetProvider"
147 + android:authorities="${applicationId}.target.drought"
148 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
149 + android:exported="true">
150 + <intent-filter>
151 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
152 + </intent-filter>
153 + </provider>
154 +
155 + <provider
156 + android:name=".targets.GitlabTargetProvider"
157 + android:authorities="${applicationId}.target.gitlab"
158 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
159 + android:exported="true">
160 + <intent-filter>
161 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
162 + </intent-filter>
163 + </provider>
164 +
165 + <provider
166 + android:name=".targets.GithubTargetProvider"
167 + android:authorities="${applicationId}.target.github"
168 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
169 + android:exported="true">
170 + <intent-filter>
171 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
172 + </intent-filter>
173 + </provider>
174 +
175 + <provider
176 + android:name=".targets.CronosTargetProvider"
177 + android:authorities="${applicationId}.target.cronos"
178 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
179 + android:exported="true">
180 + <intent-filter>
181 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
182 + </intent-filter>
183 + </provider>
184 +
185 + <provider
186 + android:name=".targets.PackagesTargetProvider"
187 + android:authorities="${applicationId}.target.packages"
188 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
189 + android:exported="true">
190 + <intent-filter>
191 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
192 + </intent-filter>
193 + </provider>
194 +
195 + <provider
196 + android:name=".targets.ConstTargetProvider"
197 + android:authorities="${applicationId}.target.const"
198 + android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_TARGETS"
199 + android:exported="true">
200 + <intent-filter>
201 + <action android:name="com.kieronquinn.app.smartspacer.TARGET" />
202 + </intent-filter>
203 + </provider>
204 +
95 205 </application>
96 206 </manifest>
added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/AqiRenderer.kt
+44 −0
@@ -0,0 +1,44 @@
1 +package io.neiam.apolloscrib.targets
2 +
3 +import io.neiam.apolloscrib.R
4 +import io.neiam.apolloscrib.types.AqiCondensed
5 +import io.neiam.apolloscrib.types.SourceType
6 +import io.neiam.apolloscrib.types.VisionEntry
7 +
8 +/**
9 + * What is in the air at a reporting area.
10 + *
11 + * The publisher sends whatever the monitor measured, so nothing is assumed
12 + * present: every reading it did send gets a row, named the way the web names
13 + * the ones it recognises.
14 + */
15 +object AqiRenderer : SourceRenderer {
16 +
17 + override val type = SourceType.Aqi
18 + override val iconRes = R.drawable.fa_lungs
19 + override val label = "Apollo's Crib: Air quality"
20 + override val description = "Pollutant readings for an area in one of your visions"
21 +
22 + override fun preview(entry: VisionEntry): List<Preview> {
23 + val readings = AqiCondensed.list(entry.data).firstOrNull() ?: return emptyList()
24 +
25 + return listOf(
26 + Preview(
27 + id = entry.key,
28 + title = readings.name ?: entry.label(),
29 + subtitle = readings.measurements.entries.firstOrNull()?.let {
30 + "${AqiCondensed.label(it.key)} ${it.value}"
31 + },
32 + iconRes = iconRes,
33 + rows = readings.measurements.map { (key, value) ->
34 + Row(
35 + iconRes = R.drawable.fa_lungs,
36 + text = AqiCondensed.label(key),
37 + stamps = listOf(Stamp(R.drawable.fa_gauge_high, value))
38 + )
39 + },
40 + empty = "No readings"
41 + )
42 + )
43 + }
44 +}
added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/CalendarRenderer.kt
+42 −0
@@ -0,0 +1,42 @@
1 +package io.neiam.apolloscrib.targets
2 +
3 +import io.neiam.apolloscrib.R
4 +import io.neiam.apolloscrib.types.CalendarCondensed
5 +import io.neiam.apolloscrib.types.SourceType
6 +import io.neiam.apolloscrib.types.VisionEntry
7 +
8 +/** What is coming up. Soonest first, which is not the order the feed sends. */
9 +object CalendarRenderer : SourceRenderer {
10 +
11 + override val type = SourceType.Calendar
12 + override val iconRes = R.drawable.fa_calendar_day
13 + override val label = "Apollo's Crib: Calendar"
14 + override val description = "Upcoming events from a calendar in one of your visions"
15 +
16 + override fun preview(entry: VisionEntry): List<Preview> {
17 + val events = entry.decode<List<CalendarCondensed>>().orEmpty()
18 + .sortedBy { it.date_start ?: "9999" }
19 +
20 + return listOf(
21 + Preview(
22 + id = entry.key,
23 + title = entry.label(),
24 + subtitle = events.firstOrNull()?.description,
25 + iconRes = iconRes,
26 + rows = events.map { event ->
27 + Row(
28 + iconRes = iconRes,
29 + text = event.description ?: "(untitled)",
30 + stamps = listOfNotNull(
31 + event.date_start?.let { Stamp(R.drawable.fa_clock, it.asDate()) }
32 + )
33 + )
34 + },
35 + empty = "Nothing coming up"
36 + )
37 + )
38 + }
39 +
40 + /** The publisher sends a date, sometimes with a time welded to it. */
41 + private fun String.asDate(): String = substringBefore('T').take(10)
42 +}
added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/DroughtRenderer.kt
+55 −0
@@ -0,0 +1,55 @@
1 +package io.neiam.apolloscrib.targets
2 +
3 +import io.neiam.apolloscrib.R
4 +import io.neiam.apolloscrib.types.DroughtCondensed
5 +import io.neiam.apolloscrib.types.SourceType
6 +import io.neiam.apolloscrib.types.VisionEntry
7 +
8 +/**
9 + * How dry it is.
10 + *
11 + * The headline is the worst category with any coverage, not a sum: the USDM
12 + * publishes cumulatively, so adding the categories up would count the same
13 + * acre several times.
14 + */
15 +object DroughtRenderer : SourceRenderer {
16 +
17 + override val type = SourceType.Drought
18 + override val iconRes = R.drawable.fa_sun_plant_wilt
19 + override val label = "Apollo's Crib: Drought"
20 + override val description = "Drought Monitor coverage for an area in one of your visions"
21 +
22 + override fun preview(entry: VisionEntry): List<Preview> {
23 + val areas = entry.decode<List<DroughtCondensed>>().orEmpty()
24 + .filter { it.mapDate != null }
25 + val latest = areas.maxByOrNull { it.mapDate.orEmpty() } ?: return emptyList()
26 +
27 + val worst = latest.worst()
28 +
29 + return listOf(
30 + Preview(
31 + id = entry.key,
32 + title = latest.where() ?: entry.label(),
33 + subtitle = worst
34 + ?.let { (category, pct) -> "$category over ${pct.asPercent()} of the area" }
35 + ?: "No drought",
36 + iconRes = iconRes,
37 + rows = listOf(
38 + "D0" to latest.d0, "D1" to latest.d1, "D2" to latest.d2,
39 + "D3" to latest.d3, "D4" to latest.d4
40 + ).mapNotNull { (category, pct) ->
41 + pct?.takeIf { it > 0.0 }?.let {
42 + Row(
43 + iconRes = iconRes,
44 + text = category,
45 + stamps = listOf(Stamp(R.drawable.fa_gauge_high, it.asPercent()))
46 + )
47 + }
48 + },
49 + empty = "No drought"
50 + )
51 + )
52 + }
53 +
54 + private fun Double.asPercent(): String = "${Math.round(this)}%"
55 +}
added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/EphemRenderer.kt
+58 −0

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/targets/GbfsRenderer.kt
+11 −5

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/GenericRenderer.kt
+72 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/PollenRenderer.kt
+55 −0

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/targets/Providers.kt
+48 −0

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/targets/Renderers.kt
+40 −2

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/TidalRenderer.kt
+64 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/targets/VcsRenderers.kt
+92 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Aqi.kt
+60 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Calendar.kt
+10 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Drought.kt
+33 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Ephem.kt
+63 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Pollen.kt
+30 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Tidal.kt
+26 −0

Click to load diff…

added clients/android/app/src/main/java/io/neiam/apolloscrib/types/Vcs.kt
+52 −0

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/types/Wire.kt
+23 −4

Click to load diff…

modified clients/android/app/src/main/java/io/neiam/apolloscrib/ui/BoardScreen.kt
+13 −1

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

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

Click to load diff…

modified clients/android/app/src/test/java/io/neiam/apolloscrib/targets/RendererTest.kt
+5 −1

Click to load diff…

modified clients/android/app/src/test/java/io/neiam/apolloscrib/types/ProdBoardTest.kt
+55 −11

Click to load diff…

modified clients/android/app/src/test/java/io/neiam/apolloscrib/types/WireTest.kt
+5 −2

Click to load diff…

modified clients/android/app/src/test/resources/prod_board.json
+1 −1

Click to load diff…

modified clients/android/tools/fa_to_vector.py
+20 −5

Click to load diff…

Parents: e1f6192