Skip to content

Commit d30fea7

Browse files
committed
Plainer credit line; easter eggs around the app
- The credit is one line in the About text: "Made by AryanPCS, with the assistance of Claude." Tapping it turns on meow mode (the bus meows, the greeting gets a paw) - Home greets you for Matariki, New Year, Waitangi Day, April Fools, Halloween, Christmas and New Year's Eve; tap a te reo greeting for what it means - Pull to refresh six times in 30 seconds, or shake four times, and the app has something to say - More things to find in search - Journeys note harbour views, the City Rail Link, long walks and long trips - Tap "Ngā Tereina" on the Trains map a few times Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011C2fAvFFcMRkaF7qSvRA1v
1 parent 2f953bf commit d30fea7

10 files changed

Lines changed: 110 additions & 36 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,5 +260,5 @@ Local builds need JDK 17, the Android SDK and Gradle 8.11:
260260
`gradle :app:assembleRelease`. Without the signing secrets, you get a
261261
debug-signed APK.
262262

263-
Made in Tāmaki Makaurau by AryanPCS, programmed with Claude (Anthropic). Not
263+
Made by AryanPCS, with the assistance of Claude. Not
264264
affiliated with Auckland Transport.

‎app/src/main/java/nz/aryan/akllive/AppViewModel.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,15 @@ class AppViewModel(app: Application) : AndroidViewModel(app) {
205205

206206
fun refresh() { kick.tryEmit(Unit) }
207207

208+
private val pulls = ArrayDeque<Long>()
209+
208210
fun pullRefresh() {
211+
// pull, pull, pull...
212+
val t = SystemClock.elapsedRealtime()
213+
pulls.addLast(t)
214+
while (pulls.isNotEmpty() && t - pulls.first() > 30_000) pulls.removeFirst()
215+
if (pulls.size == 6) toast.tryEmit(if (_settings.value.kiwi) "Easy tiger, e hoa. The buses won't come any faster 🐯"
216+
else "Easy tiger. The buses won't come any faster 🐯")
209217
_pulling.value = true
210218
refresh()
211219
liveKick.tryEmit(Unit)

‎app/src/main/java/nz/aryan/akllive/MainActivity.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class MainActivity : ComponentActivity(), SensorEventListener {
2828
private val link = MutableStateFlow<String?>(null)
2929
private var sensors: SensorManager? = null
3030
private var lastShake = 0L
31+
private var shakes = 0
3132

3233
override fun onCreate(savedInstanceState: Bundle?) {
3334
installSplashScreen()
@@ -80,10 +81,15 @@ class MainActivity : ComponentActivity(), SensorEventListener {
8081
val g = sqrt(e.values[0] * e.values[0] + e.values[1] * e.values[1] + e.values[2] * e.values[2]) / SensorManager.GRAVITY_EARTH
8182
val now = SystemClock.elapsedRealtime()
8283
if (g > 2.7f && now - lastShake > 2500) {
84+
shakes = if (now - lastShake < 12_000) shakes + 1 else 1
8385
lastShake = now
8486
if (vm.settings.value.haptics) window.decorView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
8587
vm.pullRefresh()
86-
vm.toast.tryEmit(if (vm.settings.value.kiwi) "Shaken, not stirred: refreshing" else "Refreshing")
88+
vm.toast.tryEmit(when {
89+
shakes >= 4 -> "Alright, alright! I'm refreshing 😵‍💫"
90+
vm.settings.value.kiwi -> "Shaken, not stirred: refreshing"
91+
else -> "Refreshing"
92+
})
8793
}
8894
}
8995

‎app/src/main/java/nz/aryan/akllive/Settings.kt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ data class Settings(
100100
val planOpts: PlanOpts = PlanOpts(),
101101
/** only download the 29 MB timetable on Wi-Fi */
102102
val wifiOnly: Boolean = false,
103+
/** MeowPCS mode: found by tapping the credits */
104+
val meow: Boolean = false,
103105
) {
104106
fun isFav(id: String) = favs.any { it.id == id || it.code == id }
105107
}
@@ -143,6 +145,7 @@ class Prefs(ctx: Context) {
143145
it.optBoolean("train", true), it.optBoolean("ferry", true), it.optInt("maxRides", 4))
144146
} ?: PlanOpts(),
145147
wifiOnly = p.getBoolean("wifiOnly", false),
148+
meow = p.getBoolean("meow", false),
146149
)
147150
}
148151

@@ -179,6 +182,7 @@ class Prefs(ctx: Context) {
179182
.putString("planOpts", JSONObject().put("maxWalk", o.maxWalk).put("walkSpeed", o.walkSpeed)
180183
.put("bus", o.bus).put("train", o.train).put("ferry", o.ferry).put("maxRides", o.maxRides).toString())
181184
.putBoolean("wifiOnly", s.wifiOnly)
185+
.putBoolean("meow", s.meow)
182186
.apply()
183187
}
184188

‎app/src/main/java/nz/aryan/akllive/ui/BusScreen.kt‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ fun LiveBadge(updated: Long, now: Long) {
137137
* Tap the scene and the bus honks.
138138
*/
139139
@Composable
140-
fun BusCard(b: StopBoard, now: Long, frameT: State<Float>, index: Int, weather: Weather?,
140+
fun BusCard(b: StopBoard, now: Long, frameT: State<Float>, index: Int, weather: Weather?, meow: Boolean = false,
141141
onTrack: (BusDeparture) -> Unit = {}, onOpen: () -> Unit = {}) {
142142
val next = b.departures.firstOrNull()
143143
val city = b.headsign.contains("Britomart", true) || b.headsign.contains("City", true)
@@ -196,7 +196,12 @@ fun BusCard(b: StopBoard, now: Long, frameT: State<Float>, index: Int, weather:
196196
enter = androidx.compose.animation.scaleIn() + fadeIn(),
197197
exit = fadeOut()) {
198198
Box(Modifier.background(Color.White, RoundedCornerShape(14.dp)).padding(horizontal = 12.dp, vertical = 6.dp)) {
199-
Text(if (next == null) "No bus yet! 🚏" else if (next.vehicle?.busModel()?.electric == true) "Bzzt ⚡ beep beep!" else "Beep beep! 📯",
199+
Text(when {
200+
meow -> if (next?.vehicle?.busModel()?.electric == true) "Purrrr ⚡🐱" else "Meow meow! 🐱"
201+
next == null -> "No bus yet! 🚏"
202+
next.vehicle?.busModel()?.electric == true -> "Bzzt ⚡ beep beep!"
203+
else -> "Beep beep! 📯"
204+
},
200205
color = Pal.Navy, fontWeight = FontWeight.Black)
201206
}
202207
}

‎app/src/main/java/nz/aryan/akllive/ui/HomeScreen.kt‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,38 @@ fun <T> rememberPolled(key: Any?, everyMs: Long, fetch: suspend () -> T): State<
107107
}
108108
}
109109

110-
/** Kia ora, in the right words for the time of day. */
110+
/** Matariki public holidays (a Friday that moves with the maramataka). */
111+
private val MATARIKI = setOf("2026-07-10", "2027-06-25", "2028-07-14", "2029-07-06", "2030-06-21")
112+
113+
/** A day worth a different hello: (greeting in te reo, in English, the line under it). */
114+
private fun special(): Triple<String, String, String>? {
115+
val d = Nz.now().toLocalDate()
116+
return when {
117+
d.toString() in MATARIKI -> Triple("Mānawatia a Matariki", "Happy Matariki", "Happy Māori New Year ✨")
118+
d.monthValue == 1 && d.dayOfMonth == 1 -> Triple("Ngā mihi o te Tau Hou", "Happy New Year", "New year, same bus stop 🎆")
119+
d.monthValue == 2 && d.dayOfMonth == 6 -> Triple("", "", "Happy Waitangi Day")
120+
d.monthValue == 4 && d.dayOfMonth == 1 -> Triple("", "", "Every bus is on time today. (April Fools 🤡)")
121+
d.monthValue == 10 && d.dayOfMonth == 31 -> Triple("", "", "Watch out for ghost buses 👻")
122+
d.monthValue == 12 && d.dayOfMonth == 25 -> Triple("Meri Kirihimete", "Merry Christmas", "It's a public holiday: check the times 🎄")
123+
d.monthValue == 12 && d.dayOfMonth == 31 -> Triple("", "", "Last buses of the year 🎉")
124+
else -> null
125+
}
126+
}
127+
128+
/** What the te reo greetings mean, for a tap on them. */
129+
val GREETING_MEANS = mapOf(
130+
"Mōrena" to "Mōrena: good morning",
131+
"Kia ora" to "Kia ora: hello (and thanks, and cheers)",
132+
"Ahiahi mārie" to "Ahiahi mārie: good evening",
133+
"Pō mārie" to "Pō mārie: good night",
134+
"Mānawatia a Matariki" to "Mānawatia a Matariki: celebrate Matariki, the Māori New Year",
135+
"Ngā mihi o te Tau Hou" to "Ngā mihi o te Tau Hou: happy New Year",
136+
"Meri Kirihimete" to "Meri Kirihimete: merry Christmas",
137+
)
138+
139+
/** Kia ora, in the right words for the time of day (or the day). */
111140
fun greeting(kiwi: Boolean): String {
141+
special()?.let { (reo, en, _) -> if (reo.isNotEmpty()) return if (kiwi) reo else en }
112142
val h = Nz.now().hour
113143
return if (kiwi) when (h) {
114144
in 5..11 -> "Mōrena"
@@ -134,6 +164,7 @@ fun vibe(boards: List<StopBoard>, w: Weather?, s: Settings, now: Long): String {
134164
next == null -> if (k) "Quiet out there. No buses for a bit" else "No buses coming soon"
135165
next.cancelled -> if (k) "Your ${next.route} got cancelled. Gutted." else "Your next ${next.route} is cancelled"
136166
late >= 5 -> if (k) "The ${next.route}'s running $late min late, eh" else "The ${next.route} is $late min late"
167+
special() != null -> special()!!.third
137168
w != null && w.rain >= 2 -> if (k) "Bit wet out there. Grab a brolly ☔" else "It's raining"
138169
model?.doubleDeck == true -> "Double-decker incoming: front seat upstairs? 🚌"
139170
model?.electric == true -> "Your next ${next.route} is electric ⚡"
@@ -179,7 +210,7 @@ fun HomeScreen(vm: AppViewModel) {
179210
}
180211
item { NextUp(boards, s, now) }
181212
itemsIndexed(boards, key = { _, b -> "board-" + b.code }) { i, b ->
182-
BusCard(b, now, frameT, i, weather,
213+
BusCard(b, now, frameT, i, weather, meow = s.meow,
183214
onTrack = { d -> notify { vm.track(b, d) } },
184215
onOpen = { nav.go("stop/${android.net.Uri.encode(b.code)}") })
185216
}
@@ -208,7 +239,12 @@ private fun HomeHeader(vm: AppViewModel, s: Settings, boards: List<StopBoard>, w
208239
Row(Modifier.fillMaxWidth().windowInsetsPadding(WindowInsets.statusBars).padding(top = 14.dp, start = 4.dp),
209240
verticalAlignment = Alignment.Top) {
210241
Column(Modifier.weight(1f)) {
211-
Text("${greeting(s.kiwi)},", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
242+
val hello = greeting(s.kiwi)
243+
// tap the te reo for what it means
244+
Text("$hello${if (s.meow) " 🐾" else ""},", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary,
245+
modifier = Modifier.clickable(interactionSource = null, indication = null) {
246+
GREETING_MEANS[hello]?.let { vm.toast.tryEmit(it) }
247+
})
212248
Text(s.place, style = MaterialTheme.typography.headlineLarge, maxLines = 1, overflow = TextOverflow.Ellipsis)
213249
Text(vibe(boards, w, s, now), style = MaterialTheme.typography.bodyMedium,
214250
color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp))

‎app/src/main/java/nz/aryan/akllive/ui/JourneyScreen.kt‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ private fun describe(it: Itinerary, from: Place?, to: Place?, live: Map<String,
9898
append("Planned with AKL Live")
9999
}
100100

101+
/** Little asides about a trip. */
102+
private fun tripNotes(it: Itinerary): List<String> = buildList {
103+
if (it.rides.any { r -> r.mode == Mode.Ferry }) add("⛴️ Harbour views included")
104+
if (it.rides.any { r -> r.mode == Mode.Train && r.stops.any { s -> s.stop.name.contains("Te Waihorotiu") || s.stop.name.contains("Karanga-a-Hape") } })
105+
add("🚇 Through the City Rail Link")
106+
if (it.walk >= 1500) add("👟 Good for the step count")
107+
if (it.end - it.start >= 2 * 3600) add("🥪 Pack a snack")
108+
if (it.transfers >= 3) add("🔁 Change champion")
109+
}
110+
101111
private fun RideLeg.board() = StopBoard(from.stop.code.ifEmpty { from.stop.id }, from.stop.name, from.stop.lat, from.stop.lon, route, headsign)
102112
private fun RideLeg.departure(l: TripLive?) = BusDeparture(tripId, route, headsign, from.seq, from.dep, l?.delay ?: 0,
103113
l?.delay != null, l?.cancelled == true, false, l?.seq, l?.vehicle)
@@ -230,6 +240,12 @@ private fun Steps(it: Itinerary, from: Place?, to: Place?, live: Map<String, Tri
230240
else -> Text("This one's been and gone", style = MaterialTheme.typography.titleMedium,
231241
color = MaterialTheme.colorScheme.onSurfaceVariant)
232242
}
243+
val notes = tripNotes(it)
244+
if (notes.isNotEmpty()) {
245+
Row(Modifier.horizontalScroll(rememberScrollState()).padding(top = 8.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
246+
notes.forEach { n -> Tag(n, MaterialTheme.colorScheme.tertiary) }
247+
}
248+
}
233249
Row(Modifier.horizontalScroll(rememberScrollState()).padding(vertical = 10.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
234250
it.rides.firstOrNull()?.let { r ->
235251
AssistChip({ onTrack(r) }, { Text("Track the ${r.route}") },

‎app/src/main/java/nz/aryan/akllive/ui/SearchScreen.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,18 @@ private fun quirk(q: String, kiwi: Boolean): String? = when (q.trim().lowercase(
8181
"chur", "sweet as", "sweet" -> "Chur 🤙"
8282
"bus", "buses" -> "Plenty of those. Try a route number like 27H, or a stop number off the sign."
8383
"hobbit", "hobbiton" -> "That's a bit far for an AT HOP card. 🧙"
84-
"home" -> null
84+
"l&p", "l and p", "lemon and paeroa" -> "World famous in New Zealand."
85+
"pineapple lumps" -> "🍍 Sadly not sold on board."
86+
"jandals", "jandal" -> "🩴 Perfectly acceptable bus footwear."
87+
"pav", "pavlova" -> "🍰 Ours, not Australia's."
88+
"kiwi", "🥝" -> "🥝 or 🐦? Either way, sweet as."
89+
"flat white" -> "☕ Grab one on the way to the stop."
90+
"yeah nah", "nah yeah" -> "Yeah nah, same."
91+
"ghost bus", "ghost buses" -> "👻 The one that says 2 min, then vanishes. We see you."
92+
"meow", "cat", "meowpcs" -> "🐱 Meow. Have a look at the credits in Settings."
93+
"aryanpcs" -> "That's who made this. Kia ora!"
94+
"claude" -> "👋 Kia ora, I helped build this app. Now, where are we off to?"
95+
"bus nerd" -> "Tap the version number in Settings seven times."
8596
else -> null
8697
}
8798

‎app/src/main/java/nz/aryan/akllive/ui/SettingsScreen.kt‎

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ private fun About(vm: AppViewModel) {
341341
7 -> { haptics.heavy(view); vm.toast.tryEmit("🚌 Beep beep! You found the secret depot. You're officially a bus nerd.") }
342342
}
343343
}.padding(vertical = 4.dp))
344+
val s by vm.settings.collectAsStateWithLifecycle()
345+
Text("Made by ${if (s.meow) "MeowPCS 🐾" else "AryanPCS"}, with the assistance of Claude.",
346+
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
347+
modifier = Modifier.clickable(interactionSource = null, indication = null) {
348+
haptics.tick(view)
349+
vm.update { it.copy(meow = !it.meow) }
350+
vm.toast.tryEmit(if (!s.meow) "Meow mode on 🐱" else "Meow mode off")
351+
})
344352
Text("Live data from the Auckland Transport developer API; not affiliated with Auckland Transport. " +
345353
"Directions run on AT's GTFS timetable, right on your phone. " +
346354
"Maps by MapLibre, with imagery from Esri or Toitū Te Whenua LINZ and streets from OpenFreeMap / © OpenStreetMap contributors. " +
@@ -351,30 +359,5 @@ private fun About(vm: AppViewModel) {
351359
TextButton(onClick = {
352360
try { ctx.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/intelarc/akl-live-android"))) } catch (_: Exception) { }
353361
}) { Text("Source on GitHub") }
354-
Credits()
355-
}
356-
}
357-
358-
/** Who made it. Tap the name. */
359-
@Composable
360-
private fun Credits() {
361-
var meow by rememberSaveable { mutableStateOf(false) }
362-
val view = LocalView.current
363-
val haptics = LocalHaptics.current
364-
Column(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 8.dp), horizontalAlignment = Alignment.CenterHorizontally) {
365-
HorizontalDivider(Modifier.padding(bottom = 16.dp), color = MaterialTheme.colorScheme.outlineVariant)
366-
Text("Made in Tāmaki Makaurau by", style = MaterialTheme.typography.labelMedium,
367-
color = MaterialTheme.colorScheme.onSurfaceVariant)
368-
Text(if (meow) "MeowPCS 🐾" else "AryanPCS", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Black,
369-
color = MaterialTheme.colorScheme.primary,
370-
modifier = Modifier.clip(RoundedCornerShape(12.dp)).clickable { haptics.tick(view); meow = !meow }
371-
.padding(horizontal = 12.dp, vertical = 4.dp))
372-
Spacer(Modifier.height(10.dp))
373-
Row(Modifier.clip(RoundedCornerShape(50)).background(MaterialTheme.colorScheme.surfaceContainerHigh)
374-
.padding(horizontal = 14.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
375-
Icon(Icons.Rounded.AutoAwesome, null, Modifier.size(16.dp), tint = MaterialTheme.colorScheme.tertiary)
376-
Spacer(Modifier.width(8.dp))
377-
Text("Programmed with Claude, by Anthropic", style = MaterialTheme.typography.labelLarge)
378-
}
379362
}
380363
}

‎app/src/main/java/nz/aryan/akllive/ui/TrainScreen.kt‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ fun TrainScreen(vm: AppViewModel, modifier: Modifier) {
142142
// framed when the screen opens and when "whole network" is tapped, not on every filter change
143143
val fit = remember(fitKey) { TrainNet.fit(shown) }
144144
val headings = remember { HashMap<String, Pair<Pt, Float?>>() }
145+
var titleTaps by remember { mutableIntStateOf(0) }
145146

146147
// keep a selected station's departures fresh
147148
LaunchedEffect(selStation, state.updated) { selStation?.let { vm.selectStation(it) } }
@@ -222,7 +223,10 @@ fun TrainScreen(vm: AppViewModel, modifier: Modifier) {
222223
selected = selTrain, topInset = 70f, fitKey = fitKey, links = stopsAndLinks.second,
223224
onMarker = { id -> state.trains.firstOrNull { it.vehicle.id == id }?.let(pickTrain) },
224225
onStop = { id -> id.toIntOrNull()?.let(pickStation) }, onBackground = clear)
225-
MapHeader(state, now, shown) { li ->
226+
MapHeader(state, now, shown, onTitle = {
227+
titleTaps++
228+
if (titleTaps % 5 == 0) vm.toast.tryEmit(if (titleTaps >= 15) "All aboard the tap train 🚂🚃🚃🚃" else "Choo choo! 🚂")
229+
}) { li ->
226230
filter = (if (li in shown && shown.size > 1) shown - li else shown + li).sorted()
227231
}
228232
if (!tt.ready && tt.loading) {
@@ -285,11 +289,12 @@ private fun TrainViewToggle(mode: Basemap, modifier: Modifier, set: (Basemap) ->
285289
}
286290

287291
@Composable
288-
private fun MapHeader(state: TrainState, now: Long, filter: Set<Int>, toggle: (Int) -> Unit) {
292+
private fun MapHeader(state: TrainState, now: Long, filter: Set<Int>, onTitle: () -> Unit = {}, toggle: (Int) -> Unit) {
289293
val counts = state.counts()
290294
Column(Modifier.fillMaxWidth().background(Pal.AtBlue).padding(horizontal = 14.dp, vertical = 10.dp)) {
291295
Row(verticalAlignment = Alignment.CenterVertically) {
292-
Text("Ngā Tereina", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 20.sp)
296+
Text("Ngā Tereina", color = Color.White, fontWeight = FontWeight.Bold, fontSize = 20.sp,
297+
modifier = Modifier.clickable(interactionSource = null, indication = null, onClick = onTitle))
293298
Spacer(Modifier.width(8.dp))
294299
Text("Trains · live", color = Color(0xFFBED4F0), fontSize = 14.sp)
295300
Spacer(Modifier.weight(1f))

0 commit comments

Comments
 (0)