neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
Log in to mark this repository.
Draw every source type the crate gives a shape to
084c348 · Gabriel Morell · 2026-09-02 20:40
Message
{commit_body(@commit)}
Files changed
modified
clients/android/README.md
+24
−5
| 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 | 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
| 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
| 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
| 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