diff options
| author | kkard2 <[email protected]> | 2026-06-10 14:06:55 +0200 |
|---|---|---|
| committer | kkard2 <[email protected]> | 2026-06-10 14:06:55 +0200 |
| commit | f961306d40654ac6a1ab7c262af7af74401dc693 (patch) | |
| tree | 6e2b0366f0ec077eadb815b35718312d1c79ea33 /src/frontend.h | |
init
Diffstat (limited to 'src/frontend.h')
| -rw-r--r-- | src/frontend.h | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/frontend.h b/src/frontend.h new file mode 100644 index 0000000..e9a28f4 --- /dev/null +++ b/src/frontend.h @@ -0,0 +1,350 @@ +static const char *page_html = (const char *)u8R"---(<!DOCTYPE html> +<html lang="pl"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Kontrola ekranu</title> + <style> + *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + :root { + --bg: #070707; --surf: #181818; --surf2: #212121; + --border: rgba(255,255,255,0.08); --border2: rgba(255,255,255,0.14); + --text: #e8e8e8; --muted: #888; --muted2: #999; + --accent: #4a9eff; --danger: #ff5f5f; --ok: #4caf50; + --r: 6px; + } + body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 1rem; display: flex; justify-content: center; } + .wrap { width: 100%; max-width: 400px; } + + .preview-outer { border-radius: var(--r); border: 1px solid var(--border); overflow: hidden; margin-bottom: 0.25rem; } + .preview-inner { padding: 4px; } + canvas#preview { display: block; width: 100%; height: auto; image-rendering: pixelated; border-radius: 2px; } + .preview-caption { font-size: 0.65rem; color: var(--muted); text-align: right; margin-bottom: 0.75rem; letter-spacing: 0.04em; } + + section { margin-bottom: 0.5rem; } + .sec-label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: 0.4rem; } + + .row { display: grid; gap: 0.4rem; } + .row2 { grid-template-columns: 1fr 1fr; } + .row3 { grid-template-columns: 1fr 1fr 1fr; } + + label { font-size: 0.72rem; color: var(--muted2); display: block; margin-bottom: 0.2rem; } + + textarea, select { + width: 100%; background: var(--surf); border: 1px solid var(--border); border-radius: var(--r); + color: var(--text); font-size: 0.88rem; font-family: inherit; padding: 0.45rem 0.6rem; + outline: none; transition: border-color 0.12s; + } + textarea { resize: vertical; min-height: 56px; } + textarea:focus, select:focus { border-color: var(--border2); } + + .warn { font-size: 0.7rem; color: var(--danger); min-height: 1rem; margin-top: 0.25rem; } + + .color-pick { display: flex; align-items: center; gap: 0.4rem; background: var(--surf); border: 1px solid var(--border); border-radius: var(--r); padding: 0.35rem 0.6rem; cursor: pointer; } + input[type=color] { width: 20px; height: 20px; border: none; border-radius: 3px; background: none; padding: 0; cursor: pointer; flex-shrink: 0; } + .hex { font-size: 0.75rem; font-family: monospace; color: var(--muted2); } + + .checkbox-row { display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; cursor: pointer; padding: 0.45rem 0; } + input[type=checkbox] { accent-color: var(--accent); cursor: pointer; } + + .presets { display: flex; flex-wrap: wrap; gap: 0.3rem; } + .preset { border: 1px solid var(--border); border-radius: 20px; padding: 0.2rem 0.6rem; font-size: 0.72rem; cursor: pointer; background: var(--surf); color: var(--text); transition: border-color 0.12s; display: flex; align-items: center; gap: 0.3rem; } + .preset:hover { border-color: var(--border2); } + + .send { width: 100%; padding: 0.6rem; background: var(--accent); color: #fff; border: none; border-radius: var(--r); font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: opacity 0.12s; margin-bottom: 0.35rem; } + .send:hover { opacity: 0.88; } + .wifi-off { width: 100%; padding: 0.45rem; background: transparent; color: var(--muted); border: 1px solid var(--border); border-radius: var(--r); font-size: 0.78rem; cursor: pointer; transition: color 0.12s, border-color 0.12s; } + .wifi-off:hover { color: var(--danger); border-color: var(--danger); } + + .status { text-align: center; font-size: 0.75rem; color: var(--muted); min-height: 1rem; margin-top: 0.3rem; } + .status.ok { color: var(--ok); } + .status.err { color: var(--danger); } + + hr { border: none; border-top: 1px solid var(--border); margin: 0.5rem 0; } + </style> +</head> +<body> +<div class="wrap"> + + <section> + <div class="preview-outer"> + <div class="preview-inner"> + <canvas id="preview" width="320" height="240"></canvas> + </div> + </div> + <div class="preview-caption">podgląd (nie w skali)</div> + </section> + + <section> + <div class="sec-label">Tekst</div> + <textarea id="text" maxlength="1023" placeholder="Tekst..."></textarea> + <div class="warn" id="warn"></div> + </section> + + <section> + <div class="row row3"> + <div> + <label>Kolor tekstu</label> + <div class="color-pick" id="fg-pick"> + <input type="color" id="fg" value="#ffffff"> + <span class="hex" id="fg-hex">#ffffff</span> + </div> + </div> + <div> + <label>Tło (góra)</label> + <div class="color-pick" id="bg1-pick"> + <input type="color" id="bg1" value="#000000"> + <span class="hex" id="bg1-hex">#000000</span> + </div> + </div> + <div> + <label>Tło (dół)</label> + <div class="color-pick" id="bg2-pick"> + <input type="color" id="bg2" value="#000000"> + <span class="hex" id="bg2-hex">#000000</span> + </div> + </div> + </div> + </section> + + <section> + <div class="row row2"> + <div> + <label>Skala</label> + <select id="scale"> + <option value="1">1×</option> + <option value="2" selected>2×</option> + <option value="3">3×</option> + <option value="4">4×</option> + </select> + </div> + <div style="display:flex;align-items:flex-end;"> + <label class="checkbox-row"> + <input type="checkbox" id="wrap" checked> Zawijanie słowami + </label> + </div> + </div> + </section> + + <section> + <div class="sec-label">Profil</div> + <div class="presets" id="presets"></div> + </section> + + <hr> + + <button class="send" id="send-btn">Wyślij do wyświetlacza</button> + <button class="wifi-off" id="wifi-off-btn">Wyłącz WiFi</button> + <div class="status" id="status"></div> +</div> + +<script> +const FONT_H = 8; +const FONT_WIDTHS = { + 32:3,33:2,34:4,35:8,36:6,37:8,38:7,39:2,40:4,41:4,42:5,43:6,44:3,45:4,46:2,47:5, + 48:6,49:5,50:6,51:6,52:7,53:6,54:6,55:6,56:6,57:6,58:2,59:3,60:5,61:6,62:5,63:5, + 64:10,65:7,66:7,67:7,68:7,69:6,70:6,71:7,72:7,73:2,74:5,75:7,76:6,77:8,78:7,79:7, + 80:6,81:7,82:7,83:6,84:7,85:7,86:7,87:10,88:7,89:7,90:6 +}; +function charWidth(c) { return FONT_WIDTHS[c] || 6; } + +const ISO2 = { + 260:161,261:177, // Ą ą + 262:198,263:230, // Ć ć + 280:202,281:234, // Ę ę + 321:163,322:179, // Ł ł + 323:209,324:241, // Ń ń + 211:211,243:243, // Ó ó + 346:166,347:182, // Ś ś + 379:175,380:191, // Ź ź + 377:172,378:188, // Ż ż +}; + +const SUPPORTED = new Set(Object.keys(ISO2).map(Number)); +for (let i = 32; i <= 126; i++) SUPPORTED.add(i); +SUPPORTED.add(10); + +const PRESETS = [ + { name:"Dark", fg:"#ffffff", bg1:"#000000", bg2:"#000000" }, + { name:"Dusk", fg:"#ffe8c0", bg1:"#0a0010", bg2:"#1a0830" }, + { name:"Ocean", fg:"#d0f0ff", bg1:"#000d1a", bg2:"#001a33" }, + { name:"Matrix", fg:"#00ff41", bg1:"#000000", bg2:"#001a00" }, + { name:"Ember", fg:"#ffe0a0", bg1:"#100300", bg2:"#2a0800" }, + { name:"Ice", fg:"#ffffff", bg1:"#0a1520", bg2:"#162840" }, + { name:"Mono", fg:"#cccccc", bg1:"#111111", bg2:"#1e1e1e" }, + { name:"Alert", fg:"#ffff00", bg1:"#0000ff", bg2:"#0000ff" }, +]; + +const $ = id => document.getElementById(id); + +function hexToRgb(h) { + const n = parseInt(h.slice(1), 16); + return [(n>>16)&255, (n>>8)&255, n&255]; +} + +function updatePreview() { + const canvas = $("preview"); + const ctx = canvas.getContext("2d"); + const W = canvas.width, H = canvas.height; + const fg = hexToRgb($("fg").value); + const bg1 = hexToRgb($("bg1").value); + const bg2 = hexToRgb($("bg2").value); + + const img = ctx.createImageData(W, H); + const d = img.data; + for (let y = 0; y < H; y++) { + const t = y / (H - 1); + const r = bg1[0] + (bg2[0]-bg1[0]) * t; + const g = bg1[1] + (bg2[1]-bg1[1]) * t; + const b = bg1[2] + (bg2[2]-bg1[2]) * t; + for (let x = 0; x < W; x++) { + const i = (y*W+x)*4; + d[i]=r; d[i+1]=g; d[i+2]=b; d[i+3]=255; + } + } + ctx.putImageData(img, 0, 0); + + const scale = parseInt($("scale").value); + const margin = 4; + const lineH = (FONT_H + 1) * scale; + let x = margin + 3, y = margin + 3 + 3; + const maxX = W - margin; + const xStart = x; + const text = $("text").value || "hello world"; + + ctx.fillStyle = `rgb(${fg[0]},${fg[1]},${fg[2]})`; + for (let i = 0; i < text.length; i++) { + const c = text.charCodeAt(i); + if (c === 10) { x = xStart; y += lineH; continue; } + const iso = ISO2[c] || (c <= 126 ? c : null); + if (!iso) continue; + const w = charWidth(iso) * scale; + if (x + w > maxX) { x = xStart; y += lineH; } + ctx.fillRect(x, y, w, scale); + x += (charWidth(iso) + 1) * scale; + } +} + +function updateHex(id) { $(`${id}-hex`).textContent = $(id).value; } + +function checkChars() { + const bad = []; + for (const ch of $("text").value) { + const cp = ch.codePointAt(0); + if (!SUPPORTED.has(cp) && !bad.includes(ch)) bad.push(ch); + } + $("warn").textContent = bad.length + ? "Nieobsługiwane znaki: " + bad.map(c => `'${c}'`).join(", ") + : ""; +} + +function encodeISO2(str) { + const bytes = []; + for (const ch of str) { + const cp = ch.codePointAt(0); + if (cp === 10) { bytes.push(10); continue; } + if (cp <= 126) { bytes.push(cp); continue; } + const b = ISO2[cp]; + if (b) bytes.push(b); + } + return bytes; +} + +function buildPresets() { + const c = $("presets"); + for (const p of PRESETS) { + const btn = document.createElement("button"); + btn.className = "preset"; + btn.style.background = p.bg1 === p.bg2 + ? p.bg1 + : `linear-gradient(135deg, ${p.bg1} 50%, ${p.bg2} 50%)`; + btn.style.border = `1px solid rgba(255,255,255,0.15)`; + btn.style.color = p.fg; + btn.appendChild(document.createTextNode(p.name)); + btn.onclick = () => { + $("fg").value = p.fg; $("bg1").value = p.bg1; $("bg2").value = p.bg2; + updateHex("fg"); updateHex("bg1"); updateHex("bg2"); + updatePreview(); + }; + c.appendChild(btn); + } +} + +async function sendForm() { + const bytes = encodeISO2($("text").value); + const textEncoded = bytes.map(b => b === 32 ? "+" : (b > 32 && b < 127 && !"#%&+=?".includes(String.fromCharCode(b))) ? String.fromCharCode(b) : "%" + b.toString(16).padStart(2,"0").toUpperCase()).join(""); + + const parts = [ + "text=" + textEncoded, + "fg=" + encodeURIComponent($("fg").value), + "bg1=" + encodeURIComponent($("bg1").value), + "bg2=" + encodeURIComponent($("bg2").value), + "scale=" + $("scale").value, + "wrap=" + ($("wrap").checked ? "1" : "0"), + ]; + + const btn = $("send-btn"), st = $("status"); + btn.disabled = true; btn.textContent = "Wysyłanie..."; + st.className = "status"; st.textContent = ""; + + try { + const res = await fetch("/submit", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: parts.join("&"), + }); + st.className = "status " + (res.ok ? "ok" : "err"); + st.textContent = res.ok ? "Wysłano." : "Błąd " + res.status; + } catch(e) { + st.className = "status err"; st.textContent = "Błąd sieciowy"; + } + btn.disabled = false; btn.textContent = "Wyślij do wyświetlacza"; +} + +for (const id of ["fg","bg1","bg2"]) { + $(id).addEventListener("input", () => { updateHex(id); updatePreview(); }); + $(`${id}-pick`).addEventListener("click", () => $(id).click()); +} +$("text").addEventListener("input", () => { checkChars(); updatePreview(); }); +$("scale").addEventListener("change", updatePreview); +$("wrap").addEventListener("change", updatePreview); +$("send-btn").addEventListener("click", sendForm); +$("wifi-off-btn").addEventListener("click", async () => { + const ok = confirm( + "Wyłączyć WiFi?\n\n" + + "Wyświetlacz zostanie odłączony od sieci i " + + "strona przestanie działać.\n\n" + + "Będziesz potrzebował przeprowadzić procedurę połączenia jeszcze raz (wymagany fizyczny dostęp do sprzętu)." + ); + if (!ok) return; + try { + await fetch("/wifi-off", { method: "POST" }); + } catch (_) {} + const st = $("status"); + st.className = "status err"; + st.textContent = "WiFi zostało wyłączone."; +}); + +buildPresets(); +updatePreview(); +</script> +</body> +</html>)---"; + + + + + + +static const char *ok_html = + "<!DOCTYPE html><html><head><meta charset='iso-8859-2'></head><body>" + "<h1>OK</h1><a href='/'>back</a></body></html>"; + +static const char *err_html = + "<!DOCTYPE html><html><head><meta charset='iso-8859-2'></head><body>" + "<h1>missing text field</h1><a href='/'>back</a></body></html>"; + +static const char *wifi_off_html = + "<!DOCTYPE html><html><head><meta charset='iso-8859-2'></head><body>" + "<h1>WiFi turning off</h1></body></html>"; + |
