Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 → <tag> → 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.
Expand Down
58 changes: 57 additions & 1 deletion web-image-prep/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,20 @@ <h2>1 · Source</h2>
<h2>2 · Tag profile</h2>
<input id="filter" type="search" placeholder="Search profile by name, code, or color…" style="margin-bottom:10px">
<select id="profile" size="6"></select>
<div id="custom" hidden style="margin-top:10px">
<p style="font-size:12px;color:var(--text-dim);margin:0 0 8px;line-height:1.5">
For a tag that is not listed. Use the size shown by <b>Show Tag Info</b> or set with <b>Custom Size</b> on the Flipper.
</p>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
<input id="c-w" type="number" min="8" max="800" step="1" value="296" aria-label="Width (px)" placeholder="Width">
<input id="c-h" type="number" min="8" max="800" step="1" value="128" aria-label="Height (px)" placeholder="Height">
<select id="c-color" aria-label="Color mode">
<option value="mono">Mono</option>
<option value="red">Red</option>
<option value="yellow">Yellow</option>
</select>
</div>
</div>
<div style="margin-top:10px">
<div class="stat"><span>Resolution</span><span id="pf-size">—</span></div>
<div class="stat"><span>Color mode</span><span id="pf-color">—</span></div>
Expand Down Expand Up @@ -415,13 +429,49 @@ <h3>Drop · Pick · Send</h3>
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;
Expand All @@ -435,6 +485,11 @@ <h3>Drop · Pick · Send</h3>
}
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) {
Expand Down Expand Up @@ -907,6 +962,7 @@ <h3>Drop · Pick · Send</h3>
});

/* ─── Boot ────────────────────────────────────────────────────────── */
loadCustom();
rebuildProfileList();
window.addEventListener("resize", scheduleRender);
</script>
Expand Down