App 2.45.1: Halloween's song flies across the sky on a bat's banner
Instead of a line at the foot of the hill, a bat tows a parchment banner with the title and artist through the sky above the sign. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
@@ -140,6 +140,7 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
|
||||
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
|
||||
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
|
||||
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||
- 2026-11-02 — App 2.45.1: **Halloween's song flies through the sky** (owner: "Instead of taking up screen space on the bottom, can we somehow include the title in the theme itself? ... have text fly through the air"). A bat tows a parchment banner with "♪ title — artist" from left to right across the strip of sky above the sign, behind the moon, one pass about every half minute, again and again while something plays (the title is cut first so the artist stays); the line at the foot of the hill is gone. While a banner flies the view draws about 30 frames a second, as it does while driving. Tested: emulator with the throwaway fake session (banner clear of the sign along the whole pass), unit tests, lint. Not tried on the car.
|
||||
- 2026-11-02 — App 2.45.0 published (`car-2.45.0`).
|
||||
- 2026-11-02 — App 2.45.0: **now playing on the Overview** (owner: "can we also implement music titles etc in our app?"; display only, Tiles and Halloween first). `NowPlayingWatcher` follows the active media sessions as the music-sources check reads them (`MediaSessionManager`, MEDIA_CONTENT_CONTROL as the system user), with a change listener and a callback per session, so reading costs no call into another app; it never connects to a media browser service (2.42.0) and sends no command. It runs only while the Overview is on screen in Tiles or Halloween with Settings → "Show what's playing" on (**on by default**, `PREF_NOW_PLAYING`). `NowPlaying` (pure, unit-tested) picks the session that plays, the one updated last of several, else the one paused last, passing over sessions without text (Bluetooth's empty one); CarPlay's YouTube titles lose their `[videoID]`; the radio shows its text (the artist field, e.g. "BLIND MELON - NO RAIN") with the station below. Tiles: the empty slot under the battery shows source ("· paused" when paused), title, artist, a cover (the session's picture, or Bluetooth's `ALBUM_ART_URI` file, read small off the main thread) and how far the song is; invisible when nothing plays; the narrow layout has no room and shows none. Halloween: "♪ title — artist" along the foot of the hill, the title cut first. Titles stay on screen: not logged, not sent. Tested: unit tests, lint, emulator with a throwaway build publishing a fake session (CarPlay-like title with ID removed, cover, bar; Halloween line; the switch hides and brings back both). With Halloween chosen the Settings card has two switches more and scrolls further. Not tried on the car: whether the car's sessions, Bluetooth's cover file and the radio text show as in the music-sources checks.
|
||||
- 2026-11-02 — App 2.44.0 published (`car-2.44.0`).
|
||||
|
||||
@@ -267,7 +267,8 @@ you drive the bats fly, the potion bubbles and the ghost floats (where there is
|
||||
this screen). With "Pumpkin light follows the music" on (Settings, shown with this style; off by
|
||||
default) and Bluetooth or CarPlay music playing, the pumpkin's glow flares and its carving
|
||||
brightens on each kick drum; the radio cannot be heard, so it burns as usual then. What plays
|
||||
(Bluetooth, CarPlay or the radio) is carved into the foot of the hill. The app turns the
|
||||
(Bluetooth, CarPlay or the radio) flies across the top of the sky on a parchment banner that a
|
||||
bat tows, behind the moon, again and again while it plays. The app turns the
|
||||
pumpkin's orange with it.
|
||||
|
||||

|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
||||
applicationId "com.stinnesbeck.nilsmg4"
|
||||
minSdk 27
|
||||
targetSdk 36
|
||||
versionCode 160
|
||||
versionName "2.45.0"
|
||||
versionCode 161
|
||||
versionName "2.45.1"
|
||||
|
||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
||||
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||
|
||||
@@ -27,7 +27,7 @@ import java.util.Locale;
|
||||
* of power, rising with it, in orange or, where it regenerated, green. Unread values show "—" and
|
||||
* light nothing. With music playing, the pumpkin's glow swells and its carving brightens on the
|
||||
* beat ({@link MusicTap}); colours and the embers keep their meaning, and with no music it burns as
|
||||
* before. What plays is carved into the foot of the hill ({@link NowPlaying}).
|
||||
* before. What plays flies across the sky on a banner a bat tows ({@link NowPlaying}).
|
||||
*
|
||||
* The night sky is drawn once into a bitmap per size; only the rest goes over it. Bats, bubbles and
|
||||
* the ghost move (about 30 frames a second) only while the car moves and the view is on screen;
|
||||
@@ -96,10 +96,10 @@ final class HalloweenView extends ClusterCanvasView {
|
||||
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
|
||||
c.drawBitmap(sky, 0, 0, image);
|
||||
drawBats(c, w, u);
|
||||
boolean song = drawSong(c, w, u);
|
||||
drawMoon(c, w, u);
|
||||
drawGhost(c, w, u);
|
||||
drawHill(c, w, h, u);
|
||||
drawSong(c, w, h, u);
|
||||
drawEmbers(c, w, u);
|
||||
float pulse = music == null ? 0f : music.pulse();
|
||||
drawPumpkin(c, w, u, pulse);
|
||||
@@ -107,7 +107,7 @@ final class HalloweenView extends ClusterCanvasView {
|
||||
drawFlask(c, u);
|
||||
drawTombstone(c, w, u);
|
||||
drawGears(c, w, u);
|
||||
keepAnimating(moving || pulse > 0f, 33);
|
||||
keepAnimating(moving || pulse > 0f || song, 33);
|
||||
}
|
||||
|
||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||
@@ -277,17 +277,51 @@ final class HalloweenView extends ClusterCanvasView {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- What plays, carved into the hill ----------
|
||||
// ---------- What plays: a bat tows it across the sky on a banner ----------
|
||||
|
||||
private void drawSong(Canvas c, int w, int h, float u) {
|
||||
if (v.songTitle == null) return;
|
||||
text.setTypeface(thin);
|
||||
text.setTextSize(20f * u);
|
||||
float room = w * 0.44f;
|
||||
String artist = v.songArtist == null ? "" : " — " + cut(v.songArtist, room / 2f);
|
||||
/**
|
||||
* A bat pulls a parchment banner with the song from left to right, high in the sky, behind the
|
||||
* sign and the moon, again and again while something plays. Returns whether it is drawn, so
|
||||
* the view keeps animating for it.
|
||||
*/
|
||||
private boolean drawSong(Canvas c, int w, float u) {
|
||||
if (v.songTitle == null) return false;
|
||||
text.setTypeface(face);
|
||||
text.setTextSize(19f * u);
|
||||
float room = Math.min(560f * u, w * 0.45f);
|
||||
String artist = v.songArtist == null ? "" : " — " + cut(v.songArtist, room / 2.5f);
|
||||
// A long title gives way, so the artist stays.
|
||||
String s = "♪ " + cut(v.songTitle, room - text.measureText("♪ " + artist)) + artist;
|
||||
label(c, s, w / 2f, h - 12f * u, 20f * u, AccentColor.withAlpha(BONE, 0xC0), thin, Paint.Align.CENTER);
|
||||
float t = SystemClock.uptimeMillis() / 1000f;
|
||||
float bw = text.measureText(s) + 40f * u, bh = 30f * u, tow = 56f * u;
|
||||
// One pass takes about half a minute; then it comes round again from the left.
|
||||
float travel = w + bw + tow + 80f * u;
|
||||
float bx = (t * 70f * u) % travel - bw - tow - 40f * u;
|
||||
// In the strip of sky above the sign, clear of it.
|
||||
float yc = 34f * u + (float) Math.sin(t * 1.3f) * 4f * u;
|
||||
float top = yc - bh / 2f, bottom = yc + bh / 2f, right = bx + bw, notch = 12f * u;
|
||||
// The ribbon, its trailing end cut as a swallowtail.
|
||||
path.reset();
|
||||
path.moveTo(bx, top);
|
||||
path.lineTo(right, top);
|
||||
path.lineTo(right, bottom);
|
||||
path.lineTo(bx, bottom);
|
||||
path.lineTo(bx + notch, yc);
|
||||
path.close();
|
||||
fill.setColor(BONE);
|
||||
c.drawPath(path, fill);
|
||||
line.setColor(0xFF8A6A3A);
|
||||
line.setStrokeWidth(2f * u);
|
||||
c.drawPath(path, line);
|
||||
label(c, s, bx + notch + (bw - notch) / 2f, yc + 7f * u, 19f * u, INK, face, Paint.Align.CENTER);
|
||||
// The tow lines, and the bat at their end.
|
||||
float batX = right + tow, batY = yc - 10f * u;
|
||||
line.setColor(BAT_EDGE);
|
||||
line.setStrokeWidth(1.5f * u);
|
||||
c.drawLine(right, top + 3f * u, batX, batY + 6f * u, line);
|
||||
c.drawLine(right, bottom - 3f * u, batX, batY + 6f * u, line);
|
||||
bat(c, batX, batY, 26f * u, (float) Math.sin(t * 12f), u);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** {@code s} cut with "…" to fit {@code room} in the text paint as set. */
|
||||
|
||||
Reference in New Issue
Block a user