From 30968b3d923dbd285e368da585b0104cb290cff6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 5 Mar 2026 15:18:58 +0100 Subject: [PATCH 1/4] load pixel forge tools from json file, bugfixes - load available external tools from a json file instead of hard-coding them in - add buttons to update or delete tools (instead of manually deleting files in /edit) - fixed bug that file system memory was not updated after uploading or deleting files - fixed a bug where gifs with a space would not display - fixed bug of page not loading properly (out of sync access to tabSw() ) --- wled00/data/pixelforge/pixelforge.htm | 179 ++++++++++++++++---------- wled00/wled_server.cpp | 2 + 2 files changed, 115 insertions(+), 66 deletions(-) diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm index 5fb3bb0a1d..7df813c931 100644 --- a/wled00/data/pixelforge/pixelforge.htm +++ b/wled00/data/pixelforge/pixelforge.htm @@ -391,30 +391,9 @@

Available Tokens

-
-
-

Pixel Paint

-
Interactive painting tool
- -
-
-
-
-
-

Video Lab

-
Stream video and generate animated GIFs (beta)
- -
-
-
-
-
-

PIXEL MAGIC Tool

-
Legacy pixel art editor
- -
-
-
+
+
Loading tools...
+
@@ -442,6 +421,11 @@

PIXEL MAGIC Tool

let iL=[]; // image list let gF=null,gI=null,aT=null; let fL; // file list +let pT = []; // local tools list from JSON +const remoteURL = 'https://dedehai.github.io/pf_tools.json'; // Change to your actual repo +const toolsjson = 'pf_tools.json'; +// note: the pf_tools.json must use major.minor for tool versions (e.g. 0.95 or 1.1), otherwise the update check won't work +// also the code assumes that the tool url points to a gz file // load external resources in sequence to avoid 503 errors if heap is low, repeats indefinitely until loaded (function loadFiles() { @@ -459,21 +443,19 @@

PIXEL MAGIC Tool

getLoc(); // create off screen canvas rv = cE('canvas'); - rvc = rv.getContext('2d',{willReadFrequently:true}); + rvc = rv.getContext('2d',{willReadFrequently:true}); rv.width = cv.width; rv.height = cv.height; - + tabSw(localStorage.tab||'img'); // switch to last open tab or image tab by default await segLoad(); // load available segments await flU(); // update file list - toolChk('pixelpaint.htm','t1'); // update buttons of additional tools - toolChk('videolab.htm','t2'); - toolChk('pxmagic.htm','t3'); + await loadTools(); // load additional tools list from pf_tools.json await fsMem(); // show file system memory info } /* update file list */ async function flU(){ try{ - const r = await fetch(getURL('/edit?list=/')); + const r = await fetch(getURL('/edit?list=/&cb=' + Date.now())); fL = await r.json(); }catch(e){console.error(e);} } @@ -576,14 +558,14 @@

PIXEL MAGIC Tool

await new Promise(res=>{ const im=new Image(); im.onload=()=>{ - it.style.backgroundImage=`url(${url}?cb=${Date.now()})`; + it.style.backgroundImage=`url('${encodeURI(url)}?cb=${Date.now()}')`; if(!isGif) it.style.border="5px solid red"; it.classList.remove('loading'); res(); const kb=Math.round(f.size/1024); it.title=`${name}\n${im.width}x${im.height}\n${kb} KB`; }; im.onerror=()=>{it.classList.remove('loading');it.style.background='#222';res();}; - im.src=url+'?cb='+Date.now(); + im.src=encodeURI(url)+'?cb='+Date.now(); }); } } @@ -597,35 +579,93 @@

PIXEL MAGIC Tool

//} } -/* additional tools: check if present, install if not */ -function toolChk(file, btnId) { +/* additional tools: loaded from pf_tools.json, store json locally for offline use*/ +async function loadTools() { try { - const has = fL.some(f => f.name.includes(file)); - const b = getId(btnId); - b.style.display = 'block'; - b.style.margin = '10px auto'; - if (has) { - b.textContent = 'Open'; - b.onclick = () => window.open(getURL(`/${file}`), '_blank'); // open tool: remove gz to not trigger download - } else { - b.textContent = 'Download'; - b.onclick = async () => { - const fileGz = file + '.gz'; // use gz version - const url = `https://dedehai.github.io/${fileGz}`; // always download gz version - if (!confirm(`Download ${url}?`)) return; - try { - const f = await fetch(url); - if (!f.ok) throw new Error("Download failed " + f.status); - const blob = await f.blob(), fd = new FormData(); - fd.append("data", blob, fileGz); - const u = await fetch(getURL("/upload"), { method: "POST", body: fd }); - alert(u.ok ? "Tool installed!" : "Upload failed"); - await flU(); // update file list - toolChk(file, btnId); // re-check and update button (must pass non-gz file name) - } catch (e) { alert("Error " + e.message); } - }; + const res = await fetch(getURL('/' + toolsjson + '?cb=' + Date.now())); // load local tools list + pT = res.ok ? await res.json() : []; + } catch (e) {} + + renderTools(); // render whatever we have + + try { + const rT = await (await fetch(remoteURL + '?cb=' + Date.now())).json(); + let changed = false; + rT.forEach(rt => { + let lt = pT.find(t => t.id === rt.id); + if (!lt) { + pT.push(rt); // new tool available + changed = true; + } else { + // check version + if (isNewer(rt.ver, lt.ver)) { + lt.upv = rt.ver; + changed = true; + } else if (lt.upv) { + delete lt.upv; // clean up flag if user updated or remote rolled back + changed = true; + } + lt.source = rt.source; lt.desc = rt.desc; // update info in case it changed + } + }); + if (changed) { + await saveToolsjson(); // save updated json + renderTools(); } - } catch (e) { console.error(e); } + } catch(e){console.error(e);} +} + +async function saveToolsjson() { + const fd = new FormData(); + fd.append("data", new Blob([JSON.stringify(pT)], {type:'application/json'}), toolsjson); + await fetch(getURL("/upload"), { method: "POST", body: fd }); +} + +// tool versions must be in format major.minor (e.g. 0.95 or 1.1) +function isNewer(vN, vO) { + return parseFloat(vN) > parseFloat(vO); +} + +function renderTools() { + let h = ''; + pT.forEach(t => { + const installed = fL.some(f => f.name.includes(t.file)); // check if tool file exists (either .htm or .htm.gz) + h += `
+
+

${t.name} v${t.ver}

+ ${installed ? `` : ''} +
+ ${t.desc} +
+ by ${t.author} | ${t.source} +
+
+ ${installed ? `` : ``} + ${t.upv && installed ? `` : ''} +
+
`; + }); + getId('tools').innerHTML = h || 'No tools found (offline?).'; +} + +// install or update tool +async function insT(id) { + const t = pT.find(x => x.id == id); + ovShow(); + try { + const f = await fetch(t.url); // url in json must be pointing to a gz file + if (!f.ok) throw new Error("Download failed " + f.status); + const fd = new FormData(); + fd.append("data", await f.blob(), t.file + '.gz'); // always use gz for file name (source MUST be gz) + const u = await fetch(getURL("/upload"), { method: "POST", body: fd }); + alert(u.ok ? "Tool installed!" : "Install failed"); + if (t.upv) { t.ver = t.upv; delete t.upv; } // remove update flag + await saveToolsjson(); + await flU(); // refresh file list + renderTools(); + } catch(e) { alert("Error " + e.message); } + fsMem(); // refresh memory info after upload + ovHide(); } /* fs/mem info */ @@ -1107,6 +1147,7 @@

PIXEL MAGIC Tool

} catch (e) { msg(`Error: ${e.message}`, 'err'); } finally { + fsMem(); // refresh memory info after upload ovHide(); } }; @@ -1143,7 +1184,7 @@

PIXEL MAGIC Tool

m.style.left=x+'px'; m.style.top=y+'px'; m.innerHTML=` - `; + `; d.body.appendChild(m); setTimeout(()=>{ const h=e=>{ @@ -1163,16 +1204,23 @@

PIXEL MAGIC Tool

}catch(e){msg('Download failed','err');} menuClose(); } -async function imgDel(){ - if(!confirm(`Delete ${sI.name}?`))return; + +async function deleteFile(name){ + const ins = fL.some(f => f.name.includes(name)); + if (fL.some(f => f.name === `${name}.gz`)) + name += '.gz'; // if .gz version of file exists, delete that (handles tools which are stored gzipped on device) + if(!confirm(`Delete ${name}?`))return; ovShow(); try{ - const r = await fetch(getURL(`/edit?func=delete&path=/${sI.name}`)); - if(r.ok){ msg('Deleted'); imgRm(sI.name); } + const r = await fetch(getURL(`/edit?func=delete&path=/${name}`)); + if(r.ok){ msg('Deleted'); imgRm(name); } else msg('Delete failed! File in use?','err'); }catch(e){msg('Delete failed','err');} finally{ovHide();} - menuClose(); + fsMem(); // refresh memory info after delete + menuClose(); // close menu (used for image delete button) + await flU(); // update file list + renderTools(); // re-render tools list } /* tab select and additional tools */ @@ -1186,7 +1234,6 @@

PIXEL MAGIC Tool

'Img,Txt,Oth'.split(',').forEach((s,i)=>{ getId('t'+s).onclick=()=>tabSw(['img','txt','oth'][i]); }); -tabSw(localStorage.tab||'img'); /* tokens insert */ function txtIns(el,t){ diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 79e71af54e..d002565f45 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -221,6 +221,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename, request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); } cacheInvalidate++; + updateFSInfo(); // refresh memory usage info } } @@ -310,6 +311,7 @@ static void createEditHandler() { request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Delete failed")); else request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File deleted")); + updateFSInfo(); // refresh memory usage info return; } From 34bcf01af649b07a9f13b1b28a03baaf81ed91eb Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 5 Mar 2026 20:13:55 +0100 Subject: [PATCH 2/4] fix rabbit suggestions --- wled00/data/common.js | 4 +++ wled00/data/pixelforge/pixelforge.htm | 36 ++++++++++++++++----------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/wled00/data/common.js b/wled00/data/common.js index a6223daa7c..0b66ca1a78 100644 --- a/wled00/data/common.js +++ b/wled00/data/common.js @@ -126,6 +126,10 @@ function getLoc() { } } function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } +// HTML entity escaper – use on any remote/user-supplied text inserted into innerHTML +function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); } +// URL sanitizer – blocks javascript: and data: URIs, use for externally supplied URLs for some basic safety +function safeUrl(u) { return /^https?:\/\//.test(u) ? u : '#'; } function B() { window.open(getURL("/settings"),"_self"); } var timeout; function showToast(text, error = false) { diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm index 7df813c931..013bb6f19b 100644 --- a/wled00/data/pixelforge/pixelforge.htm +++ b/wled00/data/pixelforge/pixelforge.htm @@ -391,9 +391,9 @@

Available Tokens

-
-
Loading tools...
-
+
+
Loading tools...
+
@@ -599,13 +599,9 @@

Available Tokens

} else { // check version if (isNewer(rt.ver, lt.ver)) { - lt.upv = rt.ver; - changed = true; - } else if (lt.upv) { - delete lt.upv; // clean up flag if user updated or remote rolled back + lt.pending = { ver: rt.ver, url: rt.url, file: rt.file, desc: rt.desc }; // add pending update info changed = true; } - lt.source = rt.source; lt.desc = rt.desc; // update info in case it changed } }); if (changed) { @@ -632,16 +628,16 @@

Available Tokens

const installed = fL.some(f => f.name.includes(t.file)); // check if tool file exists (either .htm or .htm.gz) h += `
-

${t.name} v${t.ver}

+

${esc(t.name)} v${t.ver}

${installed ? `` : ''}
${t.desc}
- by ${t.author} | ${t.source} + by ${esc(t.author)} | ${safeUrl(t.source)}
${installed ? `` : ``} - ${t.upv && installed ? `` : ''} + ${t.pending && installed ? `` : ''}
`; }); @@ -659,7 +655,14 @@

${t.name} v${t.ver}

fd.append("data", await f.blob(), t.file + '.gz'); // always use gz for file name (source MUST be gz) const u = await fetch(getURL("/upload"), { method: "POST", body: fd }); alert(u.ok ? "Tool installed!" : "Install failed"); - if (t.upv) { t.ver = t.upv; delete t.upv; } // remove update flag + if (u.ok && t.pending) { + // save and remove update info after successful update + t.ver = t.pending.ver; + t.url = t.pending.url; + t.file = t.pending.file; + t.desc = t.pending.desc; + delete t.pending; + } await saveToolsjson(); await flU(); // refresh file list renderTools(); @@ -1184,7 +1187,7 @@

${t.name} v${t.ver}

m.style.left=x+'px'; m.style.top=y+'px'; m.innerHTML=` - `; + `; d.body.appendChild(m); setTimeout(()=>{ const h=e=>{ @@ -1206,14 +1209,17 @@

${t.name} v${t.ver}

} async function deleteFile(name){ - const ins = fL.some(f => f.name.includes(name)); + name=name.replace('/',''); // remove leading slash if present (just in case) if (fL.some(f => f.name === `${name}.gz`)) name += '.gz'; // if .gz version of file exists, delete that (handles tools which are stored gzipped on device) if(!confirm(`Delete ${name}?`))return; ovShow(); try{ const r = await fetch(getURL(`/edit?func=delete&path=/${name}`)); - if(r.ok){ msg('Deleted'); imgRm(name); } + if(r.ok){ + msg('Deleted'); + imgRm(name); // remove image from grid (if this was not an image, this does nothing) + } else msg('Delete failed! File in use?','err'); }catch(e){msg('Delete failed','err');} finally{ovHide();} From cff13fbd991efb8280c8aa70b525cbfac53de4da Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 5 Mar 2026 20:34:30 +0100 Subject: [PATCH 3/4] minor fixes, indentation --- wled00/data/pixelforge/pixelforge.htm | 59 ++++++++++++++------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm index 013bb6f19b..27fc7d57bd 100644 --- a/wled00/data/pixelforge/pixelforge.htm +++ b/wled00/data/pixelforge/pixelforge.htm @@ -612,14 +612,14 @@

Available Tokens

} async function saveToolsjson() { - const fd = new FormData(); - fd.append("data", new Blob([JSON.stringify(pT)], {type:'application/json'}), toolsjson); - await fetch(getURL("/upload"), { method: "POST", body: fd }); + const fd = new FormData(); + fd.append("data", new Blob([JSON.stringify(pT)], {type:'application/json'}), toolsjson); + await fetch(getURL("/upload"), { method: "POST", body: fd }); } // tool versions must be in format major.minor (e.g. 0.95 or 1.1) function isNewer(vN, vO) { - return parseFloat(vN) > parseFloat(vO); + return parseFloat(vN) > parseFloat(vO); } function renderTools() { @@ -646,29 +646,30 @@

${esc(t.name)} v${t.ver}

// install or update tool async function insT(id) { - const t = pT.find(x => x.id == id); - ovShow(); - try { - const f = await fetch(t.url); // url in json must be pointing to a gz file - if (!f.ok) throw new Error("Download failed " + f.status); - const fd = new FormData(); - fd.append("data", await f.blob(), t.file + '.gz'); // always use gz for file name (source MUST be gz) - const u = await fetch(getURL("/upload"), { method: "POST", body: fd }); - alert(u.ok ? "Tool installed!" : "Install failed"); - if (u.ok && t.pending) { - // save and remove update info after successful update - t.ver = t.pending.ver; - t.url = t.pending.url; - t.file = t.pending.file; - t.desc = t.pending.desc; - delete t.pending; - } - await saveToolsjson(); - await flU(); // refresh file list - renderTools(); - } catch(e) { alert("Error " + e.message); } - fsMem(); // refresh memory info after upload - ovHide(); + const t = pT.find(x => x.id == id); + ovShow(); + try { + const src = t.pending || t; + const f = await fetch(src.url); // url in json must be pointing to a gz file + if (!f.ok) throw new Error("Download failed " + f.status); + const fd = new FormData(); + fd.append("data", await f.blob(), src.file + '.gz'); // always use gz for file name (source MUST be gz) + const u = await fetch(getURL("/upload"), { method: "POST", body: fd }); + alert(u.ok ? "Tool installed!" : "Install failed"); + if (u.ok && t.pending) { + // save and remove update info after successful update + t.ver = t.pending.ver; + t.url = t.pending.url; + t.file = t.pending.file; + t.desc = t.pending.desc; + delete t.pending; + } + await saveToolsjson(); + await flU(); // refresh file list + renderTools(); + } catch(e) { alert("Error " + e.message); } + fsMem(); // refresh memory info after upload + ovHide(); } /* fs/mem info */ @@ -1209,8 +1210,8 @@

${esc(t.name)} v${t.ver}

} async function deleteFile(name){ - name=name.replace('/',''); // remove leading slash if present (just in case) - if (fL.some(f => f.name === `${name}.gz`)) + name = name.replace('/',''); // remove leading slash if present (just in case) + if (fL.some(f => f.name.replace('/','') === `${name}.gz`)) name += '.gz'; // if .gz version of file exists, delete that (handles tools which are stored gzipped on device) if(!confirm(`Delete ${name}?`))return; ovShow(); From ec59000be07e2cc06af0a336b971e87df9ea376b Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 5 Mar 2026 20:44:26 +0100 Subject: [PATCH 4/4] improve robustness --- wled00/data/pixelforge/pixelforge.htm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/data/pixelforge/pixelforge.htm b/wled00/data/pixelforge/pixelforge.htm index 27fc7d57bd..1e1a5308db 100644 --- a/wled00/data/pixelforge/pixelforge.htm +++ b/wled00/data/pixelforge/pixelforge.htm @@ -629,15 +629,15 @@

Available Tokens

h += `

${esc(t.name)} v${t.ver}

- ${installed ? `` : ''} + ${installed ? `` : ''}
${t.desc}
by ${esc(t.author)} | ${safeUrl(t.source)}
- ${installed ? `` : ``} - ${t.pending && installed ? `` : ''} + ${installed ? `` : ``} + ${t.pending && installed ? `` : ''}
`; });