#!/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/.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()