Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1

Open
bes-r wants to merge 192 commits from bes-r/avatar-realism into main AGit
5 changed files with 99 additions and 24 deletions
Showing only changes of commit 09a5d84bba - Show all commits

View file

@ -1 +1 @@
0.3.00
0.3.01-beta

View file

@ -92,6 +92,8 @@
.tbtn:disabled{opacity:.55; cursor:default;}
#btnSave{min-width:150px; text-align:center;}
#btnSave.ok{background:var(--green); color:#fff;}
/* mislukte (auto)save: blijft rood staan tot een save wél lukt */
#toolbar #btnSave.err{background:var(--red); color:#fff;}
#btnUser.on{background:var(--accent-soft); color:var(--accent-ink);}
#langToggle{display:flex; background:var(--surface-2); border-radius:999px; padding:3px; gap:2px;}
#langToggle button{

View file

@ -3,6 +3,18 @@
Widget framework
==========================================================*/
const board = document.getElementById("board");
/* autosave-haakje: board.js meldt wijzigingen bij core.js, behalve tijdens restore */
const markBoardChange = ()=>{ if(!RESTORING) scheduleSave(); };
/* vangnet voor widget-INTERNE wijzigingen (notities typen, mindmap slepen,
schrijfblad, spelvoortgang): elke interactie in een widget-body plant een
(debounced) save - goedkoop, en dekt alle widgets zonder ze stuk voor stuk
te instrumenteren */
document.addEventListener("input", e=>{
if(e.target.closest && e.target.closest(".widget-body")) markBoardChange();
}, true);
document.addEventListener("pointerup", e=>{
if(e.target.closest && e.target.closest(".widget-body")) markBoardChange();
}, true);
const dock = document.getElementById("dock");
let zTop = 10, widgetCount = 0;
const instances = new Set();
@ -88,7 +100,7 @@ function makeWidget(def, saved){
nh = Math.max(def.minH || 180, Math.min(nh, maxH));
win.style.width = nw+"px"; win.style.height = nh+"px";
};
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); };
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); markBoardChange(); };
document.addEventListener("pointermove",mv); document.addEventListener("pointerup",up);
});
win.appendChild(rs);
@ -114,7 +126,7 @@ function makeWidget(def, saved){
win.style.zIndex = ++zTop;
}
}
minBtn.addEventListener("click", ()=>setMin(true));
minBtn.addEventListener("click", ()=>{ setMin(true); markBoardChange(); });
/* per-widget content zoom for low-vision pupils */
let wz = (saved && saved.wz) || 1;
function applyWZ(){
@ -127,6 +139,7 @@ function makeWidget(def, saved){
zBtn.addEventListener("click", ()=>{
wz = wz===1 ? 1.25 : wz===1.25 ? 1.5 : 1;
applyWZ();
markBoardChange();
});
applyWZ();
function setMax(m){
@ -148,7 +161,7 @@ function makeWidget(def, saved){
}
maxBtn.title = T(win.dataset.max==="1" ? "wUnfull" : "wFull");
}
maxBtn.addEventListener("click", ()=>setMax(win.dataset.max!=="1"));
maxBtn.addEventListener("click", ()=>{ setMax(win.dataset.max!=="1"); markBoardChange(); });
head.addEventListener("dblclick", e=>{
if(e.target===close || e.target===minBtn || e.target===maxBtn) return;
setMin(true);
@ -174,7 +187,7 @@ function makeWidget(def, saved){
win.style.left = Math.max(0, Math.min(board.clientWidth - 60, ev.clientX/zf - sx)) + "px";
win.style.top = Math.max(0, Math.min(board.clientHeight - 40, ev.clientY/zf - sy)) + "px";
};
const up = ()=>{ document.removeEventListener("pointermove",move); document.removeEventListener("pointerup",up); };
const up = ()=>{ document.removeEventListener("pointermove",move); document.removeEventListener("pointerup",up); markBoardChange(); };
document.addEventListener("pointermove",move); document.addEventListener("pointerup",up);
});
@ -185,10 +198,12 @@ function makeWidget(def, saved){
const inst = { def, win, api };
instances.add(inst);
close.addEventListener("click", ()=>{
markBoardChange();
if(pill){ pill.remove(); pill = null; }
instances.delete(inst); win.remove(); updateEmptyHint();
});
updateEmptyHint();
markBoardChange();
return inst;
}
function updateEmptyHint(){
@ -221,7 +236,11 @@ function serializeBoard(){
}))
};
}
/* tijdens het terugzetten van een bord geen autosaves plannen: het herstellen
zelf is geen wijziging (en zou anders direct na inloggen al een save triggeren) */
let RESTORING = false;
function restoreBoard(b){
RESTORING = true;
[...instances].forEach(inst=>inst.win.remove());
instances.clear();
dock.innerHTML = "";
@ -235,6 +254,7 @@ function restoreBoard(b){
board.querySelectorAll(".bimg").forEach(x=>x.remove());
(b.imgs||[]).forEach(im=>addBoardImage(im.src, im.x, im.y, im.w));
updateEmptyHint();
RESTORING = false;
}
/* ---------- board drawing layer (pen mode) ---------- */
@ -265,9 +285,11 @@ function setPen(on){
refreshPenbar();
}
function setBoardBg(bg){
const changed = BOARD.bg !== bg;
BOARD.bg = bg;
board.className = (bg && bg!=="blank") ? "bg-"+bg : "";
refreshPenbar();
if(changed) markBoardChange();
}
const PEN_TOOLS = [["hand","✋","toolHand"],["pen","✏️","toolPen"],["vulp","🖋️","toolFountain"],["mark","🖍️","toolMark"],["text","🔤","toolText"],["erase","🧽","eraser"]];
PEN_TOOLS.forEach(([id,icon])=>{
@ -646,6 +668,7 @@ bInk.addEventListener("pointermove", e=>{
});
});
const bStop = ()=>{
if(bDraw) markBoardChange();
bDraw = false;
bCtx.globalAlpha = 1;
if(DTOOLS.lens) DTOOLS.lens.lensDraw();
@ -807,7 +830,7 @@ function addBoardImage(src, x, y, w){
d.appendChild(im);
const xb = document.createElement("button");
xb.className = "dtool-x"; xb.textContent = "✕";
xb.addEventListener("click", ()=>d.remove());
xb.addEventListener("click", ()=>{ d.remove(); markBoardChange(); });
d.appendChild(xb);
const rs = document.createElement("div");
rs.className = "bimg-rs";
@ -816,7 +839,7 @@ function addBoardImage(src, x, y, w){
const zf = VZ();
const sw = d.offsetWidth, sx = e.clientX;
const mv = ev=>{ d.style.width = Math.max(80, sw + (ev.clientX - sx)/zf)+"px"; };
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); };
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); markBoardChange(); };
document.addEventListener("pointermove",mv); document.addEventListener("pointerup",up);
});
d.appendChild(rs);
@ -827,10 +850,11 @@ function addBoardImage(src, x, y, w){
const zf = VZ();
const ox = e.clientX/zf - d.offsetLeft, oy = e.clientY/zf - d.offsetTop;
const mv = ev=>{ d.style.left = (ev.clientX/zf-ox)+"px"; d.style.top = (ev.clientY/zf-oy)+"px"; };
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); };
const up = ()=>{ document.removeEventListener("pointermove",mv); document.removeEventListener("pointerup",up); markBoardChange(); };
document.addEventListener("pointermove",mv); document.addEventListener("pointerup",up);
});
board.appendChild(d);
markBoardChange();
return d;
}
document.addEventListener("langchange", refreshFab);
@ -874,6 +898,7 @@ function loadCurrentSlot(){
}
function switchTo(f, b){
stashCurrent();
markBoardChange();
BS.f = f; BS.b = b;
loadCurrentSlot();
}
@ -982,6 +1007,7 @@ function renderStrip(){
if(!fo.boards.length) fo.boards.push(newBoardEntry(T("boardName")+" 1"));
if(isCur){ BS.f = it.fi; BS.b = 0; loadCurrentSlot(); }
else if(it.fi===BS.f && it.bi < BS.b) BS.b--;
markBoardChange();
renderStrip();
});
row.appendChild(x);
@ -1026,6 +1052,7 @@ function renderBoardsPanel(){
del.addEventListener("click", ()=>{
if(fi === BS.f){ BS.folders.splice(fi,1); BS.f = 0; BS.b = 0; loadCurrentSlot(); }
else{ BS.folders.splice(fi,1); if(BS.f > fi) BS.f--; }
markBoardChange();
renderBoardsPanel();
});
fr.appendChild(del);
@ -1055,6 +1082,7 @@ function renderBoardsPanel(){
const commit = ()=>{
if(done) return; done = true;
bo.name = inp.value.trim() || bo.name;
markBoardChange();
renderBoardsPanel(); updateBoardsUI();
};
inp.addEventListener("keydown", ev=>{ ev.stopPropagation(); if(ev.key==="Enter") commit(); });
@ -1071,6 +1099,7 @@ function renderBoardsPanel(){
if(!fo.boards.length) fo.boards.push(newBoardEntry(T("boardName")+" 1"));
if(wasCur){ BS.b = 0; loadCurrentSlot(); }
else if(fi===BS.f && bi < BS.b) BS.b--;
markBoardChange();
renderBoardsPanel();
});
chip.appendChild(x);
@ -1087,6 +1116,7 @@ function renderBoardsPanel(){
const name = inp.value.trim() || `${T("folderName")} ${BS.folders.length+1}`;
BS.folders.push({ name, boards:[newBoardEntry(T("boardName")+" 1")] });
inp.value = "";
markBoardChange();
renderBoardsPanel();
};
inp.addEventListener("keydown", e=>{ e.stopPropagation(); if(e.key==="Enter") addFolder(); });

View file

@ -2,7 +2,7 @@
"use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline).
Kept in sync by hand with the VERSION file at the repo root on every release. */
const VERSION = "0.3.00";
const VERSION = "0.3.01-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -166,7 +166,8 @@ const I18N = {
errName:"Naam: 220 letters of cijfers.",
errPass:"Wachtwoord: minimaal 4 tekens.",
errCrypto:"Inloggen werkt niet in deze browser.",
hello:"Hoi", boardSaved:"Opgeslagen ✓", loginFirst:"Eerst inloggen"
hello:"Hoi", boardSaved:"Opgeslagen ✓", loginFirst:"Eerst inloggen",
saving:"Opslaan…", saveFailed:"⚠ Niet opgeslagen"
},
en:{
addWidget:"Widget", chooseWidget:"Choose a widget", saveBoard:"Save board",
@ -305,7 +306,8 @@ const I18N = {
errName:"Name: 220 letters or digits.",
errPass:"Password: at least 4 characters.",
errCrypto:"Login does not work in this browser.",
hello:"Hi", boardSaved:"Saved ✓", loginFirst:"Log in first"
hello:"Hi", boardSaved:"Saved ✓", loginFirst:"Log in first",
saving:"Saving…", saveFailed:"⚠ Not saved"
}
};
let LANG = "nl";
@ -363,18 +365,59 @@ async function api(path, opts={}){
return j;
}
/* alles van de gebruiker (borden, thema's, eigen woorden) staat op de server */
/* =========================================================
Opslaan - alles van de gebruiker staat op de server.
Autosave: elke wijziging plant (debounced) een save; de status
is altijd zichtbaar op de opslaan-knop en fouten worden NIET
meer ingeslikt (die stilte verborg de 1MB-limiet-bug).
==========================================================*/
let saveTimer = null;
let lastSaveError = null;
function canPersist(){
return !!currentUser && currentUser.role !== "pupil";
}
function persistUser(){
if(!currentUser) return Promise.resolve();
if(!canPersist()) return Promise.resolve(true);
return api("/me/data", { method:"PUT", body:{
themes: THEMES, generalExtra: GENERAL_EXTRA, boards: BS
}}).catch(()=>{});
}}).then(()=>{ lastSaveError = null; return true; })
.catch(e=>{ lastSaveError = e.message; return false; });
}
/* status op de opslaan-knop: idle / saving / saved / error */
function setSaveStatus(state){
const btn = document.getElementById("btnSave");
const lbl = btn.querySelector("span");
btn.classList.remove("ok","err");
btn.title = "";
if(state==="saving"){ lbl.textContent = T("saving"); }
else if(state==="saved"){ lbl.textContent = T("boardSaved"); btn.classList.add("ok"); }
else if(state==="error"){
lbl.textContent = T("saveFailed");
btn.classList.add("err");
btn.title = lastSaveError || "";
}
else lbl.textContent = T("saveBoard");
}
let saveStatusTimer = null;
async function doSave(){
if(!canPersist()) return false;
stashCurrent();
setSaveStatus("saving");
const ok = await persistUser();
clearTimeout(saveStatusTimer);
setSaveStatus(ok ? "saved" : "error");
/* een fout blijft staan tot een geslaagde save; succes ebt weg */
if(ok) saveStatusTimer = setTimeout(()=>setSaveStatus("idle"), 2000);
return ok;
}
/* autosave: door alles aan te roepen wat het bord of de woorden wijzigt */
function scheduleSave(){
if(!canPersist()) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(doSave, 2500);
}
function persistWords(){
if(!currentUser) return;
clearTimeout(saveTimer);
saveTimer = setTimeout(()=>{ stashCurrent(); persistUser(); }, 800);
scheduleSave();
}
function generalExtraFromData(data){
return (data && data.generalExtra) ? data.generalExtra : {nl:[], en:[]};
@ -569,8 +612,7 @@ document.getElementById("btnOwnPw").addEventListener("click", async ()=>{
/* save board */
document.getElementById("btnSave").addEventListener("click", async ()=>{
const btn = document.getElementById("btnSave");
const lbl = btn.querySelector("span");
const lbl = document.getElementById("btnSave").querySelector("span");
if(!currentUser){
lbl.textContent = T("loginFirst");
setTimeout(()=>{ lbl.textContent = T("saveBoard"); }, 2200);
@ -578,11 +620,8 @@ document.getElementById("btnSave").addEventListener("click", async ()=>{
loginWrap.classList.add("open");
return;
}
stashCurrent();
await persistUser();
lbl.textContent = T("boardSaved");
btn.classList.add("ok");
setTimeout(()=>{ lbl.textContent = T("saveBoard"); btn.classList.remove("ok"); }, 1800);
clearTimeout(saveTimer); /* expliciet opslaan gaat vóór de geplande autosave */
await doSave();
});
/* =========================================================

View file

@ -14,6 +14,10 @@ const HOST = process.env.HOST ?? '0.0.0.0';
const app = Fastify({
logger: true,
trustProxy: true, // achter nginx reverse proxy
// borden bevatten tekeningen/foto's als base64 in de JSON; de Fastify-standaard
// van 1 MB weigerde die stilletjes (413) waardoor opslaan leek te lukken maar
// niets bewaarde. Ruime limiet + eerlijke foutmelding client-side (core.js).
bodyLimit: 25 * 1024 * 1024,
});
// --- Database pool -----------------------------------------------------------