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 5fb3bb0a1d..1e1a5308db 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,97 @@

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.pending = { ver: rt.ver, url: rt.url, file: rt.file, desc: rt.desc }; // add pending update info + changed = true; + } + } + }); + if (changed) { + await saveToolsjson(); // save updated json + renderTools(); + } + } 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 += `
+
+

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

+ ${installed ? `` : ''} +
+ ${t.desc} +
+ by ${esc(t.author)} | ${safeUrl(t.source)} +
+
+ ${installed ? `` : ``} + ${t.pending && 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 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; } - } catch (e) { console.error(e); } + 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 +1151,7 @@

PIXEL MAGIC Tool

} catch (e) { msg(`Error: ${e.message}`, 'err'); } finally { + fsMem(); // refresh memory info after upload ovHide(); } }; @@ -1143,7 +1188,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 +1208,26 @@

PIXEL MAGIC Tool

}catch(e){msg('Download failed','err');} menuClose(); } -async function imgDel(){ - if(!confirm(`Delete ${sI.name}?`))return; + +async function deleteFile(name){ + 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(); 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); // 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();} - 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 +1241,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; }