diff --git a/README.md b/README.md index 76ea17d..3921f5e 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Only test tags you own or are allowed to test. ## Features - **TagTinker Flipper App:** High-performance RLE streaming IR engine. -- **TagTinker Image Prep (web):** Single-file, dependency-free HTML page that lists every supported tag profile, runs a full image pipeline (tone, contrast, detail, sharpen, dither, photo-grade Oklab 3-colour quantisation) and exports a Flipper-ready BMP. Hosted at **[i12bp8.github.io/TagTinker](https://i12bp8.github.io/TagTinker/)** (source: `web-image-prep/`). +- **TagTinker Image Prep (web):** Single-file, dependency-free HTML page that lists every supported tag profile (plus a **Custom size** entry for tags that are not listed), runs a full image pipeline (tone, contrast, detail, sharpen, dither, photo-grade Oklab 3-colour quantisation) and exports a Flipper-ready BMP. Hosted at **[i12bp8.github.io/TagTinker](https://i12bp8.github.io/TagTinker/)** (source: `web-image-prep/`). - **Drop-folder image flow:** Drop a prepared BMP into `apps_data/tagtinker/dropped/` on the Flipper SD card, then open `Targeted Payloads → → Set Image` and pick it. The Flipper rescales the 1-bit and two-plane BMPs that the image preparer exports, so one file can be sent to graphics tags of other sizes and to any page. Type 1626 (SmartTAG Color 2.6) targets only accept files up to 24 KB. - **NFC Tag Scan:** Add a target by scanning the tag's NFC chip instead of typing its barcode. This works when the tag's NFC data carries an ID TagTinker can decode; otherwise use `+ Type Barcode`. - **WiFi Plugins (optional):** Plug a Flipper WiFi Dev Board (ESP32-S2) into the GPIO header to unlock live, network-rendered tag designs — crypto price cards, weather tiles, identicons, and more — auto-discovered by the FAP. New plugins live entirely on the cloud worker; the Flipper firmware never has to be re-flashed to add one. The dev board firmware talks to the worker named by `CONFIG_TT_CLOUD_URL`, so you can point it at your own deployment of `cloud-plugins/` instead. diff --git a/web-image-prep/index.html b/web-image-prep/index.html index 269c278..7586c4d 100644 --- a/web-image-prep/index.html +++ b/web-image-prep/index.html @@ -268,6 +268,20 @@

1 · Source

2 · Tag profile

+
Resolution—
Color mode—
@@ -415,13 +429,49 @@

Drop · Pick · Send

opt.textContent = `${p.code} · ${p.w}×${p.h} · ${p.color.toUpperCase()} · ${p.name}`; profileSel.appendChild(opt); }); + /* Always offered, whatever the search: tags missing from the table + * (e.g. issue #49) still need a BMP of the right size. */ + const custom = document.createElement("option"); + custom.value = "custom"; + custom.textContent = "Custom size… · any W×H · MONO / RED / YELLOW"; + profileSel.appendChild(custom); if (profileSel.options.length && !profileSel.value) profileSel.selectedIndex = 0; applyProfile(); } -const currentProfile = () => PROFILES[parseInt(profileSel.value, 10)] ?? PROFILES[0]; + +/* Same limits as the Flipper's Custom Size screen (TAGTINKER_CUSTOM_SIZE_MIN/MAX). */ +const CUSTOM_MIN = 8, CUSTOM_MAX = 800; +const CUSTOM_KEY = "tagtinker.customProfile"; +function clampSize(v, fallback) { + const n = Math.round(Number(v)); + if (!Number.isFinite(n)) return fallback; + return Math.max(CUSTOM_MIN, Math.min(CUSTOM_MAX, n)); +} +function customProfile() { + return { + code: "custom", + w: clampSize($("c-w").value, 296), + h: clampSize($("c-h").value, 128), + color: ["mono", "red", "yellow"].includes($("c-color").value) ? $("c-color").value : "mono", + name: "Custom", + }; +} +function saveCustom() { + try { localStorage.setItem(CUSTOM_KEY, JSON.stringify(customProfile())); } catch {} +} +function loadCustom() { + try { + const c = JSON.parse(localStorage.getItem(CUSTOM_KEY) || "null"); + if (c) { $("c-w").value = c.w; $("c-h").value = c.h; $("c-color").value = c.color; } + } catch {} +} +const currentProfile = () => profileSel.value === "custom" + ? customProfile() + : (PROFILES[parseInt(profileSel.value, 10)] ?? PROFILES[0]); function applyProfile() { const p = currentProfile(); + $("custom").hidden = p.code !== "custom"; $("pf-size").textContent = `${p.w} × ${p.h}`; $("pf-color").textContent = p.color.toUpperCase(); $("pf-type").textContent = p.code; @@ -435,6 +485,11 @@

Drop · Pick · Send

} filter.addEventListener("input", rebuildProfileList); profileSel.addEventListener("change", applyProfile); +["c-w", "c-h", "c-color"].forEach(id => $(id).addEventListener("input", () => { saveCustom(); applyProfile(); })); +/* Snap out-of-range sizes back into the field once the user leaves it. */ +["c-w", "c-h"].forEach(id => $(id).addEventListener("change", () => { + $(id).value = clampSize($(id).value, id === "c-w" ? 296 : 128); saveCustom(); applyProfile(); +})); /* ─── Bindings ────────────────────────────────────────────────────── */ function bindRange(id, key, fmt = v => v) { @@ -907,6 +962,7 @@

Drop · Pick · Send

}); /* ─── Boot ────────────────────────────────────────────────────────── */ +loadCustom(); rebuildProfileList(); window.addEventListener("resize", scheduleRender);