summaryrefslogtreecommitdiff
path: root/src/frontend.h
diff options
context:
space:
mode:
authorkkard2 <[email protected]>2026-06-10 14:06:55 +0200
committerkkard2 <[email protected]>2026-06-10 14:06:55 +0200
commitf961306d40654ac6a1ab7c262af7af74401dc693 (patch)
tree6e2b0366f0ec077eadb815b35718312d1c79ea33 /src/frontend.h
init
Diffstat (limited to 'src/frontend.h')
-rw-r--r--src/frontend.h350
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&times;</option>
+ <option value="2" selected>2&times;</option>
+ <option value="3">3&times;</option>
+ <option value="4">4&times;</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>";
+