Minamp 1.6: adjustable UI size, larger text and controls

- New setting "Text and button size" (Normal / Large / Extra large): scales
  text, icons and buttons on top of the phone's density (App.UI_SCALES)
- Larger default text, rows, tiles, touch targets and player icons; all sizes
  now live in res/values/dimens.xml
- Seek bar knob drawn in code, since Android 2.x caches XML shapes at the
  size they were first loaded
- Full-width buttons wrap to two lines instead of cutting words off
- Shorter labels so they fit 240x320 screens in both languages
- tools/check_fit.py checks that UI texts fit their slots at every size and
  language using Droid Sans metrics; build.sh runs it and setup_toolchain.sh
  fetches the font

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xockoandClaude Opus 5.5 committed 2026-10-15 08:02:49 +02:00
1 parent fa67e95fde
commit bbdab09f83
32 files changed
+457 -190

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
"""Checks that UI texts fit their slots on a 240x320 ldpi screen, in every language and UI size.
Uses Droid Sans (Android 2.x's system font) metrics. A slot's width is computed from the screen
width in dp, which shrinks as the UI scale grows (240 px / (0.75 * scale)). Texts that are
expected to ellipsize (album titles etc.) aren't checked; fixed UI words are.
Run: python3 tools/check_fit.py [font_dir]
"""
import os
import sys
from PIL import ImageFont
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from strings import S # noqa: E402
FONT_DIR = sys.argv[1] if len(sys.argv) > 1 else "/home/claude/tools/fonts/x/connect-fonts-droidsans/package/fonts/latin"
REG = ImageFont.truetype(os.path.join(FONT_DIR, "droidsans-regular.ttf"), 200)
BOLD = ImageFont.truetype(os.path.join(FONT_DIR, "droidsans-bold.ttf"), 200)
SCALES = {"Normal": 1.0, "Grande": 1.15, "Moi grande": 1.3} # keep in sync with App.UI_SCALES
SCREEN_PX = 240
LDPI = 0.75
SAFETY = 1.04 # hinting / rounding headroom
def width_dp(text, sp, bold):
f = BOLD if bold else REG
return f.getlength(text) * sp / 200.0 * SAFETY
def lines_needed(text, sp, bold, avail):
words = text.replace("\n", " \n ").split(" ")
lines, cur = 1, ""
for w in words:
if w == "\n":
lines += 1
cur = ""
continue
trial = (cur + " " + w).strip()
if width_dp(trial, sp, bold) <= avail:
cur = trial
else:
if cur:
lines += 1
cur = w
if width_dp(w, sp, bold) > avail:
return 99 # a single word doesn't fit
return lines
def texts(keys, fmt_args=()):
out = []
for k in keys:
en, gl = S[k]
for lang, t in (("en", en), ("gl", gl)):
if fmt_args:
try:
t = t.replace("%1$s", fmt_args[0]).replace("%1$d", fmt_args[0]).replace("%2$d", fmt_args[1] if len(fmt_args) > 1 else "")
except Exception:
pass
out.append((k, lang, t))
return out
# slot: (name, keys, sp, bold, width_fn(screen_dp), max_lines, fmt_args)
def slots(D):
sw = lambda W: W # noqa: E731
return [
("tab", ["tab_home", "tab_artists", "tab_albums", "tab_playlists"], D["text_tab"], True,
lambda W: W / 4 - 4, 1, ()),
("detail button", ["play", "shuffle", "play_all", "artist_radio"], D["text_button"], True,
lambda W: (W - 2 * 10 - 10) / 2 - 2 * 4, 1, ()),
("home tile title", ["library_radio", "loved_radio", "time_travel", "random_album"], D["text_tile"], True,
lambda W: W - 2 * 8 - 12 - 10 - D["tile_icon"] - 12, 1, ()),
("home tile sub", ["library_radio_sub", "loved_radio_sub", "time_travel_sub", "random_album_sub"], D["text_small"], False,
lambda W: W - 2 * 8 - 12 - 10 - D["tile_icon"] - 12, 1, ()),
("shelf header + see all", ["recently_added", "recently_played"], D["text_small"], True,
lambda W: W - 10 - width_dp(S["see_all"][1], D["text_small"], True) - 20, 1, ()),
("now playing: playing from", ["playing_from"], D["text_tiny"], True,
lambda W: W - 2 * D["touch"] - 8, 1, ()),
("now playing: sleep", ["sleep", "end_of_track"], D["text_np_small"], False,
lambda W: W - 16 - 16 - D["np_small_icon"] - 6 - width_dp(S["album_link"][1], D["text_np_small"], False) - 16, 1, ()),
("now playing: album link", ["album_link"], D["text_np_small"], False,
lambda W: (W - 24) / 2 - 16, 1, ()),
("setup button", ["sign_in", "connect_address", "pin_check_now", "cancel", "connect", "back"], D["text_button"], True,
lambda W: W - 2 * 20 - 2 * D["button_pad"], 1, ()),
("settings: size options", ["size_normal", "size_large", "size_xlarge"], 18, False, lambda W: W - 80, 1, ()),
("setup tagline", ["tagline"], D["text_sub"], False, lambda W: W - 40, 3, ()),
("pin status", ["pin_not_yet"], D["text_sub"], False, lambda W: W - 40 - 24, 3, ("12", "11")),
("pin instructions", ["pin_open"], D["text_sub"], False, lambda W: W - 40, 2, ()),
("queue title", ["up_next"], D["text_title"], True, lambda W: W - 3 * D["touch"] - 12, 1, ()),
("search hint", ["search_hint"], D["text_body"], False, lambda W: W - D["touch"] - 12 - 20, 1, ()),
("status message", ["queue_empty", "no_playlists", "pick_something", "nothing_playing"], D["text_status"], False,
lambda W: W - 48, 2, ()),
]
def run(D):
bad = 0
for scale_name, s in SCALES.items():
W = SCREEN_PX / (LDPI * s)
for name, keys, sp, bold, wfn, max_lines, args in slots(D):
avail = wfn(W)
for k, lang, t in texts(keys, args):
n = lines_needed(t, sp, bold, avail)
if n > max_lines:
bad += 1
print("DOESN'T FIT %-11s %-26s %s %-24r needs %s line(s) in %.0fdp (max %d)"
% (scale_name, name, lang, t, "word too long:" if n == 99 else n, avail, max_lines))
return bad
if __name__ == "__main__":
import re
dimens = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "res", "values", "dimens.xml")).read()
D = {m.group(1): float(m.group(2)) for m in re.finditer(r'<dimen name="(\w+)">([\d.]+)(?:sp|dp)</dimen>', dimens)}
n = run(D)
print("all texts fit" if n == 0 else "%d text(s) don't fit" % n)
sys.exit(1 if n else 0)
+8
View File
@@ -54,6 +54,14 @@ PY
python3 "$PROJ/tools/patch_bc_dalvik.py" bcsrc
fi
echo "== Droid Sans (Android 2.x's font), for tools/check_fit.py"
if [ ! -f fonts/droidsans-regular.ttf ]; then
mkdir -p fonts dl/fonts
curl -sSL -o dl/droid.tgz "$(curl -sS https://registry.npmjs.org/connect-fonts-droidsans/latest | python3 -c 'import json,sys;print(json.load(sys.stdin)["dist"]["tarball"])')"
tar xzf dl/droid.tgz -C dl/fonts
cp dl/fonts/package/fonts/latin/droidsans-regular.ttf dl/fonts/package/fonts/latin/droidsans-bold.ttf fonts/
fi
echo "== signing key"
if [ ! -f minamp.keystore ]; then
if [ -f "$PROJ/signing/minamp.keystore" ]; then cp "$PROJ/signing/minamp.keystore" .; else
+19 -14
View File
@@ -10,10 +10,10 @@ S = {
"app_name": ("Minamp", "Minamp"),
# tabs / headers
"tab_home": ("HOME", "INICIO"),
"tab_artists": ("ARTISTS", "ARTISTAS"),
"tab_albums": ("ALBUMS", "ÁLBUMS"),
"tab_playlists": ("PLAYLISTS", "LISTAS"),
"tab_home": ("Home", "Inicio"),
"tab_artists": ("Artists", "Artistas"),
"tab_albums": ("Albums", "Álbums"),
"tab_playlists": ("Playlists", "Listas"),
"playing_from": ("PLAYING FROM", "REPRODUCINDO DESDE"),
"see_all": ("SEE ALL ›", "VER TODO ›"),
"album_link": ("Album ›", "Álbum ›"),
@@ -21,12 +21,12 @@ S = {
# setup
"tagline": ("Your Plex music, anywhere,\non a very small phone.", "A túa música de Plex, onde queiras,\nnun teléfono moi pequeno."),
"sign_in": ("Sign in with Plex", "Iniciar sesión con Plex"),
"connect_address": ("Connect to a server address", "Conectar a un enderezo de servidor"),
"connect_address": ("Connect by address", "Conectar por enderezo"),
"connection_log": ("Connection log", "Rexistro de conexión"),
"pin_open": ("On a computer or a newer phone, open", "Nun ordenador ou nun teléfono máis novo, abre"),
"pin_enter": ("and enter this code:", "e escribe este código:"),
"pin_waiting": ("Waiting for you to link…", "Agardando a que vincules a conta…"),
"pin_check_now": ("I've entered it — check now", "Xa o escribín: comprobar agora"),
"pin_check_now": ("Check now", "Comprobar agora"),
"cancel": ("Cancel", "Cancelar"),
"server_address": ("Server address", "Enderezo do servidor"),
"server_address_help": ("e.g. 192.168.1.20 or https://plex.example.com. Signing in with Plex is easier and finds remote addresses for you.",
@@ -90,9 +90,9 @@ S = {
# actions
"play": ("Play", "Reproducir"),
"play_all": ("Play all", "Reproducir todo"),
"play_all": ("Play all", "Reproducir"),
"shuffle": ("Shuffle", "Aleatorio"),
"artist_radio": ("Artist radio", "Radio do artista"),
"artist_radio": ("Radio", "Radio"),
"artist_radio_shuffle": ("Artist radio (shuffle)", "Radio do artista (aleatorio)"),
"play_next": ("Play next", "Reproducir a continuación"),
"add_to_queue": ("Add to queue", "Engadir á cola"),
@@ -129,6 +129,11 @@ S = {
"set_connection": ("Connection: %1$s", "Conexión: %1$s"),
"set_log": ("Connection log & streaming test", "Rexistro de conexión e proba de reprodución"),
"set_language": ("Language: %1$s", "Idioma: %1$s"),
"set_size": ("Text and button size: %1$s", "Tamaño de texto e botóns: %1$s"),
"size_title": ("Text and button size", "Tamaño de texto e botóns"),
"size_normal": ("Normal", "Normal"),
"size_large": ("Large", "Grande"),
"size_xlarge": ("Extra large", "Moi grande"),
"sign_out": ("Sign out", "Pechar sesión"),
"quality_home_title": ("Quality at home", "Calidade na casa"),
"quality_away_title": ("Quality away / mobile", "Calidade fóra / datos móbiles"),
@@ -158,11 +163,11 @@ S = {
"loved_radio": ("Favourites radio", "Radio de favoritas"),
"loved_radio_sub": ("Your top rated", "As mellor valoradas"),
"time_travel": ("Time travel", "Viaxe no tempo"),
"time_travel_sub": ("Pick a decade", "Escolle unha década"),
"time_travel_sub": ("Pick a decade", "Escolle década"),
"random_album": ("Random album", "Álbum ao chou"),
"random_album_sub": ("Feeling lucky", "Proba sorte"),
"recently_added": ("Recently added", "Engadidos recentemente"),
"recently_played": ("Recently played", "Escoitados recentemente"),
"recently_added": ("Recently added", "Novidades"),
"recently_played": ("Recently played", "Últimas escoitas"),
"finding_decades": ("Finding decades…", "Buscando décadas…"),
"no_years": ("No release years in this library", "Esta biblioteca non ten anos de publicación"),
"surprise_me": ("Surprise me", "Sorpréndeme"),
@@ -192,7 +197,7 @@ S = {
"nothing_playing": ("Nothing playing", "Non se está a reproducir nada"),
"pick_something": ("Pick something from your library", "Escolle algo da túa biblioteca"),
"end_of_track": ("End of track", "Fin da pista"),
"sleep": ("Sleep", "Temporizador"),
"sleep": ("Sleep", "Durmir"),
"sleep_timer": ("Sleep timer", "Temporizador de apagado"),
"sleep_off": ("Off", "Desactivado"),
"sleep_15": ("15 minutes", "15 minutos"),
@@ -211,7 +216,7 @@ S = {
"paused_dot": ("Paused · ", "En pausa · "),
# queue
"up_next": ("Up next", "A continuación"),
"up_next": ("Queue", "Cola"),
"shuffle_on": ("Shuffle on", "Aleatorio activado"),
"shuffle_off": ("Shuffle off", "Aleatorio desactivado"),
"remove_from_queue": ("Remove from queue", "Quitar da cola"),
@@ -220,7 +225,7 @@ S = {
"clear_everything": ("Clear everything", "Borralo todo"),
# search
"search_hint": ("Artists, albums, tracks", "Artistas, álbums, cancións"),
"search_hint": ("Search music", "Buscar música"),
"search_in": ("Search %1$s", "Buscar en %1$s"),
"searching": ("Searching…", "Buscando…"),
"nothing_found_q": ("Nothing found for “%1$s”", "Non se atopou nada para “%1$s”"),