Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/api/qr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import io

import segno
from fastapi import APIRouter, HTTPException
from fastapi.responses import Response

router = APIRouter()

_MAX_LEN = 500


@router.get("/qr")
def get_qr(url: str) -> Response:
if not url or len(url) > _MAX_LEN:
raise HTTPException(status_code=422, detail="url required (max 500 chars)")
qr = segno.make_qr(url, error="m")
buf = io.BytesIO()
qr.save(buf, kind="svg", scale=5, border=2, dark="#1a1206", light="#ffffff")
return Response(
content=buf.getvalue(),
media_type="image/svg+xml",
headers={"Cache-Control": "public, max-age=3600"},
)
2 changes: 2 additions & 0 deletions app/api/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
from app.api.config import router as config_router
from app.api.events import router as events_router
from app.api.jobs import router as jobs_router
from app.api.qr import router as qr_router
from app.api.stems import router as stems_router

router = APIRouter()
router.include_router(config_router, tags=["config"])
router.include_router(jobs_router, prefix="/jobs", tags=["jobs"])
router.include_router(events_router, tags=["events"])
router.include_router(stems_router, tags=["stems"])
router.include_router(qr_router, tags=["qr"])
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ dependencies = [
# decompression-bomb bypass fixed in 2.7.0. urllib3 is a transitive
# dep via requests; pin floor to pull in the fix.
"urllib3>=2.7.0",
# Pure-Python QR code generator used by the /api/qr endpoint (server
# access QR codes in the desktop settings panel).
"segno>=1.6",
]

[project.optional-dependencies]
Expand Down
12 changes: 9 additions & 3 deletions static/css/daw.css
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ input, textarea { font-family: inherit; }
.settings-tab.active { color: var(--fg); border-bottom-color: var(--accent); }
.settings-pane { display: flex; flex-direction: column; min-height: 0; }
.settings-pane[data-pane="general"] { flex: 1; }
.settings-pane[data-pane="advanced"] { flex: 1; overflow-y: auto; }
.settings-pane.hidden { display: none; }
.settings-pane[data-pane="advanced"] .library-editor-table-wrap { flex: none; max-height: 240px; margin-bottom: 2px; }
.settings-empty { color: var(--muted); font-size: 12px; text-align: center; padding: 28px 10px; }
Expand Down Expand Up @@ -807,10 +808,15 @@ input, textarea { font-family: inherit; }
.settings-switch.disabled input { cursor: default; }
.settings-net { margin-top: 10px; font-size: 11px; color: var(--muted); }
.settings-net.hidden { display: none; }
.settings-net-label { margin-bottom: 7px; }
.settings-net-list { display: flex; flex-direction: column; gap: 5px; align-items: flex-start; }
.settings-net-list code { color: var(--accent); background: rgba(244,183,64,0.1); padding: 3px 9px; border-radius: 5px; font-size: 11.5px; }
.settings-net-empty { color: var(--muted); }
.settings-net-qr { display: flex; flex-direction: column; gap: 14px; margin-top: 6px; }
.qr-hint { font-size: 10px; color: var(--muted); margin: 0; line-height: 1.4; }
.qr-cards-row { display: flex; flex-wrap: wrap; gap: 28px; }
.qr-card { display: flex; flex-direction: column; align-items: center; gap: 6px; cursor: pointer; }
.qr-img-wrap { width: 130px; height: 130px; border-radius: 6px; border: 3px solid var(--accent); overflow: hidden; box-sizing: border-box; }
.qr-card img { width: 100%; height: 100%; display: block; transition: filter 0.25s ease, scale 0.25s ease; }
.qr-card.qr-blurred img { filter: blur(10px); scale: 1.12; }
.qr-label { font-size: 9.5px; color: var(--muted); text-align: center; max-width: 130px; word-break: break-all; }
.settings-server-note { font-size: 10.5px; color: var(--muted); margin: 0 0 12px; line-height: 1.5; }
.settings-subhead { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); font-weight: 600; margin: 4px 0 7px; }

Expand Down
44 changes: 33 additions & 11 deletions static/js/catalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -1836,8 +1836,7 @@ function networkSettingsHtml() {
</label>
</div>
<div class="settings-net hidden">
<div class="settings-net-label">Access on your local network by any of these addresses:</div>
<div class="settings-net-list"></div>
<div class="settings-net-qr"></div>
</div>
</div>
`;
Expand Down Expand Up @@ -1897,7 +1896,7 @@ async function wireGeneralSettings(overlay) {
async function wireNetworkSetting(overlay) {
const input = overlay.querySelector(".net-access-input");
const netWrap = overlay.querySelector(".settings-net");
const list = overlay.querySelector(".settings-net-list");
const qrWrap = overlay.querySelector(".settings-net-qr");
if (!input) return;

let enabled = false;
Expand All @@ -1911,21 +1910,44 @@ async function wireNetworkSetting(overlay) {
}
} catch { /* leave defaults */ }

// Build the address list with textContent (URLs are server data, but never
// interpolate untrusted strings into innerHTML).
if (list) {
list.textContent = "";
// QR codes: one per LAN address, each encodes the /mobile/ URL so the
// phone camera opens StemDeck directly. Cards start blurred so an open
// camera app on a nearby device doesn't scan them before you're ready.
if (qrWrap) {
qrWrap.textContent = "";
if (addresses.length) {
const hint = document.createElement("p");
hint.className = "qr-hint";
hint.textContent = "Blurred so your camera doesn't get too excited. Tap to reveal.";
qrWrap.appendChild(hint);
const row = document.createElement("div");
row.className = "qr-cards-row";
for (const a of addresses) {
const code = document.createElement("code");
code.textContent = a;
list.appendChild(code);
const mobileUrl = `${a}/mobile/`;
const card = document.createElement("div");
card.className = "qr-card qr-blurred";
card.title = "Tap to unblur";
card.addEventListener("click", () => card.classList.toggle("qr-blurred"));
const img = document.createElement("img");
img.src = `/api/qr?url=${encodeURIComponent(mobileUrl)}`;
img.alt = `QR code for ${mobileUrl}`;
img.width = 130;
img.height = 130;
const label = document.createElement("div");
label.className = "qr-label";
label.textContent = mobileUrl;
const imgWrap = document.createElement("div");
imgWrap.className = "qr-img-wrap";
imgWrap.appendChild(img);
card.append(imgWrap, label);
row.appendChild(card);
}
qrWrap.appendChild(row);
} else {
const span = document.createElement("span");
span.className = "settings-net-empty";
span.textContent = "No local network connection detected.";
list.appendChild(span);
qrWrap.appendChild(span);
}
}

Expand Down
11 changes: 11 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.