Grafik: km/h radbröts i aktivitetslistan, trenddiagrammet startade på tomma framtidsdagar, "1 reps"; tools/waydroid-ui.py
All checks were successful
release / build-release (push) Successful in 4m22s

Klickrunda i Waydroid (telefonlayout 411×891 dp) genom alla flikar, inställningar,
mål, aktiviteter, passdetalj, PB-sök, detaljark, våg och om-sidan.

- Aktiviteter: "km/h" bröts till "km/" + "h" när kcal-kolumnen trängde ihop
  raden – word joiner (U+2060) efter snedstrecket håller enheten ihop.
- Statistik → Volym per dag/vecka: LazyRow scrollade till sista stapeln, dvs.
  månadens tomma framtidsdagar. Nu: senaste stapel med volym, annars dagens
  stapel, annars längst till höger.
- LiftStatusSheet: "Topplista · 1 reps" → "1 rep".
- tools/waydroid-ui.py: adb-hjälpare (dump/tap/shot/swipe/text/key) för
  klickrundor i Waydroid; README beskriver flödet + wm size/density-tricket.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGT3xpdm51McmSrJcHnrzq
This commit is contained in:
2026-09-10 00:50:21 +02:00
parent 213f635d93
commit 9a7b564e49
5 changed files with 85 additions and 4 deletions

View File

@@ -52,6 +52,12 @@ med `waydroid session start` + `waydroid show-full-ui`, sedan
Saknar Android nät bortom värden är det Dockers FORWARD-spärr — se infra-Doc Saknar Android nät bortom värden är det Dockers FORWARD-spärr — se infra-Doc
`hosts/brasse-linux01.md` (Waydroid-avsnittet). Testkonto: `bb01`. `hosts/brasse-linux01.md` (Waydroid-avsnittet). Testkonto: `bb01`.
Skalskriptet `tools/waydroid-ui.py` (kräver `pillow`) gör klickrundor enkla: `dump` listar
texter med koordinater, `tap "Statistik"` trycker på en text, `shot namn` sparar en
skärmdump av appfönstret i `/tmp/shots/`, `swipe up|down` scrollar. Waydroid-fönstret är
en bred surfplatta; för telefonlayout sätt `adb shell wm size 720x1560 && adb shell wm
density 280` (≈ 411×891 dp) och återställ med `wm size reset && wm density reset`.
### Testa mot API:t ### Testa mot API:t
Logga in med ditt vanliga gym-konto (LDAP). API-url:en går att ändra under Logga in med ditt vanliga gym-konto (LDAP). API-url:en går att ändra under

View File

@@ -435,7 +435,9 @@ private fun ActivityRow(a: Activity, onClick: () -> Unit, onShowMap: (() -> Unit
a.distanceMeters?.let { a.distanceMeters?.let {
append(" · ${(it / 1000).compact()} km") append(" · ${(it / 1000).compact()} km")
val kmh = (it / 1000) / (a.durationSeconds / 3600.0) val kmh = (it / 1000) / (a.durationSeconds / 3600.0)
append(" · ${kmh.compact()} km/h") // U+2060 (word joiner) efter snedstrecket: annars radbryts "km/h"
// till "km/" + "h" när kcal-kolumnen tränger ihop raden.
append(" · ${kmh.compact()} km/\u2060h")
} }
a.rpe?.let { append(" · RPE ${it.toInt()}") } a.rpe?.let { append(" · RPE ${it.toInt()}") }
}, },

View File

@@ -233,7 +233,7 @@ fun LiftStatusSheet(
// ── Topplista ── // ── Topplista ──
if (candidates.isNotEmpty()) { if (candidates.isNotEmpty()) {
Text( Text(
"Topplista · ${target.reps} reps — tryck för att välja ett annat lyft", "Topplista · ${target.reps} ${if (target.reps == 1) "rep" else "reps"} — tryck för att välja ett annat lyft",
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(top = 6.dp), modifier = Modifier.padding(top = 6.dp),
) )

View File

@@ -650,9 +650,19 @@ private fun TrendBars(stats: StatsBundle) {
val max = stats.trend.maxOf { it.volumeKg }.takeIf { it > 0 } ?: 1.0 val max = stats.trend.maxOf { it.volumeKg }.takeIf { it > 0 } ?: 1.0
val listState = androidx.compose.foundation.lazy.rememberLazyListState() val listState = androidx.compose.foundation.lazy.rememberLazyListState()
// Börja längst till höger — senaste perioden är den intressanta. // Börja vid senaste stapeln MED volym: servern skickar hela perioden (t.ex.
// resten av månaden), och står vi längst till höger ser man bara tomma
// framtidsdagar. Ingen volym alls: dagens stapel (etikett "d/M") om den
// finns, annars längst till höger.
androidx.compose.runtime.LaunchedEffect(stats.trend.size) { androidx.compose.runtime.LaunchedEffect(stats.trend.size) {
if (stats.trend.isNotEmpty()) listState.scrollToItem(stats.trend.size - 1) if (stats.trend.isNotEmpty()) {
val today = java.time.LocalDate.now().let { "${it.dayOfMonth}/${it.monthValue}" }
val target = stats.trend.indexOfLast { it.volumeKg > 0 }
.takeIf { it >= 0 }
?: stats.trend.indexOfFirst { it.label == today }.takeIf { it >= 0 }
?: (stats.trend.size - 1)
listState.scrollToItem(target)
}
} }
androidx.compose.foundation.lazy.LazyRow( androidx.compose.foundation.lazy.LazyRow(

63
tools/waydroid-ui.py Normal file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Liten adb-hjälpare för att styra FitnessDroid i Waydroid (eller på en telefon) från skalet.
Användning: python3 tools/waydroid-ui.py <dump|tap <text>|shot <namn> [skala]|swipe up|down [andel]|text <str>|key <KEYCODE>|tapxy <x> <y>>
Skärmdumpar hamnar i /tmp/shots/. Enhet via env ADB_SERIAL (standard Waydroid 192.168.240.112:5555).
Telefonlik yta i Waydroid: adb shell wm size 720x1560 && adb shell wm density 280 (återställ: wm size reset; wm density reset).
"""
import re, subprocess, sys, time
from PIL import Image
import os
D = ["adb", "-s", os.environ.get("ADB_SERIAL", "192.168.240.112:5555")]
def sh(*a, **k): return subprocess.run(D + list(a), capture_output=True, text=True, **k).stdout
def nodes():
sh("shell", "uiautomator", "dump", "/sdcard/ui.xml")
x = sh("shell", "cat", "/sdcard/ui.xml")
out = []
for m in re.finditer(r'<node[^>]*?>', x):
n = m.group(0)
g = lambda k: (re.search(k + r'="([^"]*)"', n) or [None, ""])[1]
b = re.search(r'bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', n)
if not b: continue
out.append(dict(cls=g("class").split(".")[-1], text=g("text").replace("&amp;", "&").replace("&#127942;", "🏆"), desc=g("content-desc"),
b=tuple(map(int, b.groups())), enabled=g("enabled"), checked=g("checked"), selected=g("selected")))
return out
def win(ns=None):
ns = ns or nodes()
for n in ns:
if n["desc"].startswith("Caption bar of FitnessDroid"): return n["b"]
return (0, 0, 2560, 1408)
def center(b): return (b[0] + b[2]) // 2, (b[1] + b[3]) // 2
def cmd_dump():
ns = nodes(); w = win(ns)
print("WIN", w)
for n in ns:
if n["text"] or n["desc"] or n["cls"] in ("EditText", "Switch", "CheckBox", "RadioButton"):
flags = "".join(f for f, v in (("E", n["enabled"] == "false"), ("", n["checked"] == "true"), ("S", n["selected"] == "true")) if v)
print(f'{n["cls"]:10} {n["text"][:90]!r:95} {n["desc"][:30]!r:34} {n["b"]} {flags}')
def cmd_tap(label, idx=0):
ns = nodes(); hits = [n for n in ns if n["text"] == label or n["desc"] == label]
if not hits: hits = [n for n in ns if label.lower() in (n["text"] + n["desc"]).lower()]
if not hits: print("NOTFOUND", label); return 1
x, y = center(hits[int(idx)]["b"]); sh("shell", "input", "tap", str(x), str(y)); print("TAP", label, x, y)
def cmd_shot(name, scale=0.5):
scale = float(scale)
ns = nodes(); w = win(ns)
os.makedirs("/tmp/shots", exist_ok=True)
png = subprocess.run(D + ["exec-out", "screencap", "-p"], capture_output=True).stdout
open("/tmp/shots/_full.png", "wb").write(png)
im = Image.open("/tmp/shots/_full.png").crop(w)
if scale != 1: im = im.resize((int(im.width * scale), int(im.height * scale)))
p = f"/tmp/shots/{name}.png"; im.save(p); print("SHOT", p, im.size, "win", w)
def cmd_swipe(direction, frac=0.6):
frac = float(frac)
w = win(); x = w[0] + int((w[2] - w[0]) * 0.3); h = w[3] - w[1]
y1, y2 = (w[1] + int(h * 0.85), w[1] + int(h * (0.85 - frac))) if direction == "up" else (w[1] + int(h * 0.25), w[1] + int(h * (0.25 + frac)))
sh("shell", "input", "swipe", str(x), str(y1), str(x), str(y2), "500"); print("SWIPE", direction)
def cmd_text(s): sh("shell", "input", "text", s.replace(" ", "%s")); print("TEXT", s)
def cmd_key(k): sh("shell", "input", "keyevent", k); print("KEY", k)
def cmd_tapxy(x, y): sh("shell", "input", "tap", x, y); print("TAPXY", x, y)
if __name__ == "__main__":
a = sys.argv[1:]; c = a[0]
{"dump": lambda: cmd_dump(), "tap": lambda: cmd_tap(*a[1:]), "shot": lambda: cmd_shot(*a[1:]),
"swipe": lambda: cmd_swipe(*a[1:]), "text": lambda: cmd_text(a[1]), "key": lambda: cmd_key(a[1]), "tapxy": lambda: cmd_tapxy(a[1], a[2])}[c]()