From 84db0c03809b1430cb8f6efd3100abfbe8db9d98 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Fri, 21 Aug 2026 18:03:35 +0200 Subject: [PATCH] =?UTF-8?q?QR=20k=C3=B3d=20se=20d=C3=A1=20p=C5=99epnout=20?= =?UTF-8?q?na=20jinou=20adresu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tabule dosud zbylé adresy jen vypsala, ale naskenovat se dala pouze ta první. Teď se na ně dá kliknout: vybraná se přesune nahoru velkým písmem, QR kód se přepne na ni a ta předchozí se objeví v seznamu, takže se dá kroužit dokola. Kódy jsou vykreslené všechny předem a přepíná se jen ten viditelný — žádné doptávání serveru, žádná QR knihovna na klientovi. Aby to stránku nenafouklo, skládá se SVG ze souvislých úseků místo jednotlivých čtverečků; tabule se třemi adresami má 23 kB. Otisk v data-address zahrnuje všechny adresy, ne jen tu první. Když se změní kterákoli z nich, nabídka i QR kódy jsou k ničemu a tabule se načte znovu. Co-Authored-By: Claude Opus 5 --- README.md | 4 +- static/style.css | 22 ++++++++++- templates/_join.html | 54 +++++++++++++++++++++----- templates/_panel.html | 2 +- templates/board.html | 2 +- tests/test_web.py | 90 +++++++++++++++++++++++++++++++++++++++++++ web.py | 55 +++++++++++++++++--------- 7 files changed, 197 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index cc03a0a..9a0b596 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ Počitadlo je dostupné na: ``` Na notebooku otevři `/board`, naskenuj QR kód telefonem a hraj. Tabule -zobrazuje i zbylé adresy — když jedna nefunguje, zkus další. Zastavuje se -`Ctrl+C`. +nabízí i zbylé adresy — když jedna nefunguje, klikni na jinou a QR kód se +přepne na ni. Zastavuje se `Ctrl+C`. | Proměnná | K čemu je | Výchozí | |---|---|---| diff --git a/static/style.css b/static/style.css index ce95cdb..9284f3e 100644 --- a/static/style.css +++ b/static/style.css @@ -373,7 +373,7 @@ button { line-height: 1.4; } -.join .address span { +.join .address .what { display: block; font-family: var(--font-ui); font-size: 0.7rem; @@ -382,6 +382,8 @@ button { color: var(--ink-soft); } +.join .address .url { display: block; } + .qr { width: 5.5rem; height: 5.5rem; fill: var(--ink); } .idle { @@ -480,6 +482,24 @@ button { .alt li { white-space: nowrap; } +/* Adresa je tlačítko, ale nemá vypadat jako tlačítko — jen dát najevo, + že se na ni dá kliknout. */ +.alt button { + font: inherit; + color: inherit; + background: none; + border: 0; + border-bottom: 1px dotted var(--rule); + padding: 0; + cursor: pointer; +} + +.alt button:hover, +.alt button:focus-visible { + color: var(--ink); + border-bottom-color: currentColor; +} + .alt em { font-family: var(--font-ui); font-style: normal; diff --git a/templates/_join.html b/templates/_join.html index a9126c7..ee1a39c 100644 --- a/templates/_join.html +++ b/templates/_join.html @@ -1,22 +1,58 @@ -{# Kudy se na počitadlo dostane telefon. Nabídneme víc cest, vybrat si může člověk. #} -{% if primary %} +{# Kudy se na počitadlo dostane telefon. Nabídneme víc cest, vybrat si může + člověk — QR kódy jsou vykreslené všechny, přepíná se jen ten viditelný. #} +{% if choices %}
- {{ target|qr }} +
+ {% for choice in choices %} +
+ {{ choice.url|qr }} +
+ {% endfor %} +
+
- Zapisuje se z telefonu - {{ target }} - {% if others %} + Zapisuje se z telefonu + {{ choices[0].url }} + + {% if choices|length > 1 %}
    - {% for address in others %} -
  • {{ address.ip }}:{{ port }} {{ address.label }}
  • + {% for choice in choices %} +
  • + +
  • {% endfor %}
{% endif %}
+ + {% else %}
-

Adresu se nepodařilo zjistit +

Adresu se nepodařilo zjistit Spusť server s DICE_HOST=<adresa>.

{% endif %} diff --git a/templates/_panel.html b/templates/_panel.html index ef24f89..72195af 100644 --- a/templates/_panel.html +++ b/templates/_panel.html @@ -1,3 +1,3 @@ {# Fragment pro tabuli: obě části, které se během hry mění. #} - +
{% include "_sheet.html" %}
diff --git a/templates/board.html b/templates/board.html index a430dfc..277b751 100644 --- a/templates/board.html +++ b/templates/board.html @@ -4,7 +4,7 @@ {% block body %}
- + {% include "_join.html" %}
diff --git a/tests/test_web.py b/tests/test_web.py index b10660e..e6d28a7 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,5 +1,8 @@ +import re + import pytest +import net import storage import web @@ -236,3 +239,90 @@ def test_kratka_hra_nic_neuseka(client): ]) def test_sklonovani_kol(count, expected): assert web.kola(count) == expected + + +def offer(monkeypatch, *ips): + """Server nabídne přesně tyhle adresy, v tomhle pořadí.""" + found = [net.Address(ip=ip, interface="test", kind="lan", primary=not i) + for i, ip in enumerate(ips)] + monkeypatch.setattr(web.net, "addresses", lambda: found) + + +def test_tabule_nabidne_qr_ke_kazde_adrese(client, monkeypatch): + """Kódy jsou vykreslené předem, přepíná se jen ten viditelný.""" + offer(monkeypatch, "192.168.0.10", "100.91.0.24", "172.17.0.1") + game_id = start_game(client, "Adam", "Eva") + + page = text(client.get(f"/board/{game_id}")) + + assert page.count('
  • ' in page + + +def test_jedina_adresa_nema_co_prepinat(client, monkeypatch): + offer(monkeypatch, "192.168.0.10") + + page = text(client.get("/board")) + + assert '
      ' not in page + assert page.count(' str: - """Adresa, na kterou tabule ukazuje. Prázdno, když se nedá zjistit.""" - found = net.addresses() - return found[0].ip if found else "" +def address_key() -> str: + """Otisk všech adres. Tabule ho hlídá: jakmile se změní, načte se znovu, + protože nabídnuté adresy i QR kódy už neplatí.""" + return ",".join(address.ip for address in net.addresses()) def links(path: str = "/") -> dict: - """Adresy, na kterých je počitadlo dostupné, i s cílem pro QR kód. + """Adresy, na kterých je počitadlo dostupné, i s cíli pro QR kódy. Hledá se při každém načtení — notebook se během večera může přesunout - z domácí WiFi na hotspot a zpátky. + z domácí WiFi na hotspot a zpátky. První v pořadí je ta nejnadějnější, + zbytek si člověk může na tabuli přepnout. """ - found = net.addresses() - primary = found[0] if found else None - return { - "primary": primary, - "others": found[1:], + "choices": [ + {"ip": address.ip, "label": address.label, + "url": address.url(PORT) + path} + for address in net.addresses() + ], "port": PORT, - "target": primary.url(PORT) + path if primary else "", } +def runs(row: list[bool]) -> list[tuple[int, int]]: + """Souvislé tmavé úseky řádku jako (začátek, délka). + + Jeden široký obdélník místo řady čtverečků. Na tabuli visí QR kód pro + každou adresu zvlášť, takže se to na velikosti stránky pozná. + """ + found = [] + start = None + + for x, dark in enumerate([*row, False]): + if dark and start is None: + start = x + elif not dark and start is not None: + found.append((start, x - start)) + start = None + + return found + + def qr_svg(data: str) -> Markup: - """QR jako SVG složené ze čtverečků — bez Pillow a bez práce s obrázky.""" + """QR jako SVG složené z obdélníků — bez Pillow a bez práce s obrázky.""" code = qrcode.QRCode(box_size=1, border=2) code.add_data(data) code.make(fit=True) @@ -93,14 +112,14 @@ def qr_svg(data: str) -> Markup: size = len(matrix) rects = [ - f'' + f'' for y, row in enumerate(matrix) - for x, dark in enumerate(row) if dark + for x, width in runs(row) ] return Markup( f'{"".join(rects)}' ) @@ -184,7 +203,7 @@ def view(game: Game, game_id: int, row) -> dict: "standings": game.standings(), "winner": winner, "current": "" if winner else game.current_player, - "primary_ip": primary_ip(), + "address_key": address_key(), } -- 2.47.3