Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5
5 changed files with 99 additions and 24 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.00
|
||||
0.3.01-beta
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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(); });
|
||||
|
|
|
|||
|
|
@ -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: 2–20 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: 2–20 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();
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
|
|
|
|||
|
|
@ -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 -----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue