From 9a7b564e49f87456606a31a97591c271bb8058dc Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 10 Sep 2026 00:50:21 +0200 Subject: [PATCH] =?UTF-8?q?Grafik:=20km/h=20radbr=C3=B6ts=20i=20aktivitets?= =?UTF-8?q?listan,=20trenddiagrammet=20startade=20p=C3=A5=20tomma=20framti?= =?UTF-8?q?dsdagar,=20"1=20reps";=20tools/waydroid-ui.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01NGT3xpdm51McmSrJcHnrzq --- README.md | 6 ++ .../ui/activities/ActivitiesScreen.kt | 4 +- .../fitnessdroid/ui/common/LiftStatusSheet.kt | 2 +- .../fitnessdroid/ui/stats/StatsScreen.kt | 14 ++++- tools/waydroid-ui.py | 63 +++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 tools/waydroid-ui.py diff --git a/README.md b/README.md index 172c779..62aafdc 100644 --- a/README.md +++ b/README.md @@ -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 `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 Logga in med ditt vanliga gym-konto (LDAP). API-url:en går att ändra under diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/activities/ActivitiesScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/activities/ActivitiesScreen.kt index bfc7073..d924db3 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/activities/ActivitiesScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/activities/ActivitiesScreen.kt @@ -435,7 +435,9 @@ private fun ActivityRow(a: Activity, onClick: () -> Unit, onShowMap: (() -> Unit a.distanceMeters?.let { append(" · ${(it / 1000).compact()} km") 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()}") } }, diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/LiftStatusSheet.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/LiftStatusSheet.kt index 4bf5861..39a95b1 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/LiftStatusSheet.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/common/LiftStatusSheet.kt @@ -233,7 +233,7 @@ fun LiftStatusSheet( // ── Topplista ── if (candidates.isNotEmpty()) { 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, modifier = Modifier.padding(top = 6.dp), ) diff --git a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt index 0851467..df0f306 100644 --- a/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt +++ b/app/src/main/java/eu/brassepc/fitnessdroid/ui/stats/StatsScreen.kt @@ -650,9 +650,19 @@ private fun TrendBars(stats: StatsBundle) { val max = stats.trend.maxOf { it.volumeKg }.takeIf { it > 0 } ?: 1.0 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) { - 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( diff --git a/tools/waydroid-ui.py b/tools/waydroid-ui.py new file mode 100644 index 0000000..663ebc7 --- /dev/null +++ b/tools/waydroid-ui.py @@ -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 |shot [skala]|swipe up|down [andel]|text |key |tapxy > +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']*?>', 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("&", "&").replace("🏆", "🏆"), 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]()