Files
lintunes/andtunes/tools/make_buttons.py
T
travandClaude Opus 5 9483d8951b v0.19.0: andTunes plays music on the Rabbit
The Android half of andTunes had been paused on a 1 GB toolchain download
(Gradle wrapper, Android Gradle Plugin, Kotlin). None of that was needed:
platform 33 and build-tools 34 were already in ~/Android/Sdk, and an app that
links no libraries needs five SDK steps, not a build system. andtunes/build.py
runs aapt2 -> javac -> R8 -> zipalign -> apksigner and produces a 61 KiB APK.

andTunes 0.1.0 is framework-only Java: a six-tile menu that loads nothing, a
streaming library.json parse grouped into artists and albums in one pass, one
ListActivity for every list (songs and artist songs open with Shuffle all,
albums carry art thumbs, search as you type), a Now playing bar under every
list, and a white Now Playing screen with square art, a slim scrubber,
prev/play/next, shuffle and repeat, and no back button. A foreground
PlaybackService owns the queue: audio focus, becoming-noisy, MediaSession and
a MediaStyle notification, end of queue pauses, missing files skip, and the
last queue and position come back on launch. Black on white, and every size
doubled for the R1's override density. Cold start to the menu: 313-338 ms.

The scroll wheel turned out to be DPAD_UP/DOWN (stock Generic.kl), not
volume. The app takes both in dispatchKeyEvent: lists scroll a row per
detent, and the menu and Now Playing get volume.

LinTunes side: Connections > Install andTunes on Rabbit... installs the
committed APK over adb and grants all-files access, or copies it to Download/
over MTP when there's no adb. Sync fills Buttons/ with default artwork only
where a file is missing, so trav's own art is never overwritten.

Verified against the dev library only, synced to the R1 and driven with
adb input + screencap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSsYtRn4WZx6xj4GEdVPt9
2026-09-11 14:25:55 -05:00

136 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Render andTunes' default button artwork.
These are the stand-ins trav can replace: LinTunes copies each one into
``Music/andTunes/Buttons/`` on the Rabbit only when that file is missing, and
the app draws ``Buttons/<name>.png`` in preference to its bundled copy. So the
names here are a contract with both halves — change one and the user's own art
for it silently stops being used.
python3 andtunes/tools/make_buttons.py
Writes the same PNGs to the app's ``res/drawable-nodpi/`` (bundled fallback)
and to ``lintunes/andtunes/buttons/`` (what sync ships). Black and white only:
the R1's panel is dim and contrast is the only thing that reads on it.
"""
import os
import sys
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QPointF, QRectF, Qt # noqa: E402
from PyQt6.QtGui import ( # noqa: E402
QColor, QFont, QGuiApplication, QImage, QPainter, QPainterPath, QPolygonF,
)
HERE = Path(__file__).resolve().parent
RES = HERE.parent / "app" / "src" / "main" / "res" / "drawable-nodpi"
SHIP = HERE.parent.parent / "lintunes" / "andtunes" / "buttons"
# The menu: a 2 × 3 grid on a 480-dp-wide screen, so ~240 × 208 per tile.
TILE_W, TILE_H = 240, 208
MENU = {
"playlists": "Playlists",
"artists": "Artists",
"albums": "Albums",
"songs": "Songs",
"search": "Search",
"now_playing": "Now Playing",
"shuffle_all": "Shuffle All",
}
ICON = 128
TRANSPORT = ("prev", "play", "pause", "next")
def _canvas(w, h):
image = QImage(w, h, QImage.Format.Format_ARGB32)
image.fill(Qt.GlobalColor.transparent)
painter = QPainter(image)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
return image, painter
def tile(label: str) -> QImage:
image, p = _canvas(TILE_W, TILE_H)
path = QPainterPath()
path.addRoundedRect(QRectF(4, 4, TILE_W - 8, TILE_H - 8), 20, 20)
p.fillPath(path, QColor("black"))
font = QFont("DejaVu Sans")
font.setPixelSize(40)
font.setBold(True)
p.setFont(font)
p.setPen(QColor("white"))
p.drawText(QRectF(12, 12, TILE_W - 24, TILE_H - 24),
int(Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap),
label)
p.end()
return image
def _tri(x0, y0, x1, y1, pointing_right=True) -> QPolygonF:
if pointing_right:
pts = [(x0, y0), (x1, (y0 + y1) / 2), (x0, y1)]
else:
pts = [(x1, y0), (x0, (y0 + y1) / 2), (x1, y1)]
return QPolygonF([QPointF(x, y) for x, y in pts])
def transport(name: str, color="black", size=ICON) -> QImage:
image, p = _canvas(size, size)
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor(color))
s = size / 128
if name == "play":
p.drawPolygon(_tri(34 * s, 20 * s, 110 * s, 108 * s))
elif name == "pause":
p.drawRect(QRectF(28 * s, 20 * s, 26 * s, 88 * s))
p.drawRect(QRectF(74 * s, 20 * s, 26 * s, 88 * s))
elif name == "next":
p.drawPolygon(_tri(18 * s, 28 * s, 86 * s, 100 * s))
p.drawRect(QRectF(88 * s, 28 * s, 16 * s, 72 * s))
elif name == "prev":
p.drawPolygon(_tri(42 * s, 28 * s, 110 * s, 100 * s, False))
p.drawRect(QRectF(24 * s, 28 * s, 16 * s, 72 * s))
p.end()
return image
def launcher() -> QImage:
size = 192
image, p = _canvas(size, size)
path = QPainterPath()
path.addRoundedRect(QRectF(8, 8, size - 16, size - 16), 36, 36)
p.fillPath(path, QColor("black"))
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor("white"))
p.drawPolygon(_tri(70, 50, 142, 96))
p.drawPolygon(QPolygonF([QPointF(70, 96), QPointF(142, 96),
QPointF(70, 142)]))
p.end()
return image
def save(image: QImage, name: str, *dirs: Path):
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
if not image.save(str(d / f"{name}.png"), "PNG"):
sys.exit(f"could not write {d / name}.png")
def main():
app = QGuiApplication(sys.argv) # noqa: F841 — must outlive the painting
for name, label in MENU.items():
save(tile(label), name, RES, SHIP)
for name in TRANSPORT:
save(transport(name), name, RES, SHIP)
save(launcher(), "ic_launcher", RES)
# Status-bar icons must be white-on-transparent; Android tints by alpha.
save(transport("play", "white", 96), "ic_notify", RES)
print(f"wrote {len(MENU) + len(TRANSPORT)} buttons to {RES} and {SHIP}")
if __name__ == "__main__":
main()