Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4

Open
bes-r wants to merge 207 commits from bes-r/kleurplaten-v0-4-66 into main AGit
3 changed files with 95 additions and 17 deletions
Showing only changes of commit a919e31e4c - Show all commits

View file

@ -1 +1 @@
0.3.28-beta
0.3.29-beta

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.28-beta";
const VERSION = "0.3.29-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;
@ -88,6 +88,8 @@ const I18N = {
shGlobal:"Systeem", shSchool:"School",
shPublished:"Gedeeld ✓", shCopied:"Gekopieerd naar je eigen mappen ✓",
shNoCreateHere:"Nieuw maken kan alleen in je eigen mappen - sleep het daarna hierheen om te delen.",
shReadonly:"Alleen lezen - kopieer het naar je eigen mappen om te bewerken.",
exLock:"Alleen lezen", shLockedOk:"Vergrendeld ✓", shUnlockedOk:"Ontgrendeld ✓",
hudMin:"Menu inklappen/uitklappen", hudOrient:"Horizontaal of verticaal",
wFull:"Maximaliseren", wUnfull:"Terugzetten",
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
@ -267,6 +269,8 @@ const I18N = {
shGlobal:"System", shSchool:"School",
shPublished:"Shared ✓", shCopied:"Copied to your own folders ✓",
shNoCreateHere:"Create it in your own folders first - then drag it here to share.",
shReadonly:"Read-only - copy it to your own folders to edit.",
exLock:"Read-only", shLockedOk:"Locked ✓", shUnlockedOk:"Unlocked ✓",
hudMin:"Collapse/expand menu", hudOrient:"Horizontal or vertical",
wFull:"Maximize", wUnfull:"Restore",
wZoom:"Content bigger or smaller (low vision)",

View file

@ -121,6 +121,9 @@
document.removeEventListener("pointermove", move);
document.removeEventListener("pointerup", up);
if(!dragging) return;
/* de click ná een sleep mag geen map openen/selecteren */
el.dataset.justDragged = "1";
setTimeout(()=>{ delete el.dataset.justDragged; }, 0);
ghost.remove();
if(target){
target.classList.remove("ex-drop");
@ -212,46 +215,79 @@
showResult(res);
});
}
/* mappen openen met één klik, dus zonder selectie werken hernoemen en
verwijderen op de map waar je nu in staat (het laatste broodkruimel) */
tool("✏ " + T("exRename"), ()=>{
if(!SEL){ msg(T("exSelectFirst")); return; }
if(!SEL){
if(!PATH.length){ msg(T("exSelectFirst")); return; }
const crumbs = crumbsEl.querySelectorAll(".ex-crumb");
const last = crumbs[crumbs.length-1];
const oldName = PATH[PATH.length-1];
inlineInput(last, oldName, async (n)=>{
const res = await AD.renameFolder(PATH.slice(0,-1), oldName, n);
if(!res) PATH = [...PATH.slice(0,-1), n];
render();
showResult(res);
});
return;
}
const t = gridEl.querySelector(".ex-tile.sel .ex-name");
if(!t) return;
const cur = SEL.type==="folder" ? SEL.key : t.textContent;
inlineInput(t, cur, async (n)=>{
const res = SEL.type==="folder" ? await AD.renameFolder(PATH, SEL.key, n) : await AD.renameItem(SEL.key, n);
inlineInput(t, t.textContent, async (n)=>{
const res = await AD.renameItem(SEL.key, n);
SEL = null;
render();
showResult(res);
});
});
const delBtn = tool("🗑 " + T("exDelete"), async ()=>{
if(!SEL){ msg(T("exSelectFirst")); return; }
if(!SEL && !PATH.length){ msg(T("exSelectFirst")); return; }
if(!delBtn.classList.contains("danger")){
delBtn.classList.add("danger");
delBtn.textContent = "🗑 " + T("exConfirm");
return;
}
const res = SEL.type==="folder" ? await AD.deleteFolder(PATH, SEL.key) : await AD.deleteItem(SEL.key);
let res;
if(SEL){ res = await AD.deleteItem(SEL.key); }
else{
res = await AD.deleteFolder(PATH.slice(0,-1), PATH[PATH.length-1]);
if(!res) PATH = PATH.slice(0,-1);
}
SEL = null;
render();
showResult(res);
});
/* optioneel (gedeelde school-tak): alleen-lezen aan/uit op het
geselecteerde item of anders op de huidige map */
if(AD.lockLabel){
const ll = AD.lockLabel(PATH);
if(ll) tool(ll, async ()=>{
const res = await AD.toggleLock(SEL, PATH);
SEL = null;
render();
showResult(res);
});
}
/* raster: eerst mappen, dan items */
gridEl.innerHTML = "";
const { folders, items } = AD.list(PATH);
folders.forEach(f=>{
const t = tileEl("folder" + (SEL && SEL.type==="folder" && SEL.key===f.name ? " sel" : ""));
const t = tileEl("folder");
t.dataset.drop = [...PATH, f.name].join("/");
const ico = document.createElement("div");
ico.className = "ex-ico"; ico.textContent = "📁";
t.appendChild(ico);
t.appendChild(nameEl(f.name));
const c = document.createElement("div");
c.className = "ex-count"; c.textContent = f.count;
c.className = "ex-count"; c.textContent = (f.locked ? "🔒 " : "") + f.count;
t.appendChild(c);
t.addEventListener("click", ()=>{ SEL = {type:"folder", key:f.name}; markSel(t); });
t.addEventListener("dblclick", ()=>{ PATH = [...PATH, f.name]; SEL = null; render(); msg(""); });
/* één klik opent de map (Windows-webstijl); slepen blijft werken via
de bewegingsdrempel, en de click direct een sleep wordt genegeerd */
t.addEventListener("click", ()=>{
if(t.dataset.justDragged) return;
PATH = [...PATH, f.name]; SEL = null; render(); msg("");
});
makeDraggable(t, {type:"folder", key:f.name, movable: f.fixed !== true});
gridEl.appendChild(t);
});
@ -269,7 +305,10 @@
hn.className = "ex-hint"; hn.textContent = it.hint;
t.appendChild(hn);
}
t.addEventListener("click", ()=>{ SEL = {type:"item", key:it.key}; markSel(t); });
t.addEventListener("click", ()=>{
if(t.dataset.justDragged) return;
SEL = {type:"item", key:it.key}; markSel(t);
});
t.addEventListener("dblclick", async ()=>{
const res = await AD.openItem(it.key);
if(typeof res === "string"){ render(); msg(res); return; }
@ -357,7 +396,7 @@ function withSharedBranches(kind, own, hooks){
items.push({
key: "sh:"+it.id, name: it.name,
icon: kind === "board" ? "▦" : "⚓",
hint: it.ownerName || undefined,
hint: [it.canManage ? "" : "🔒", it.ownerName || ""].filter(Boolean).join(" ") || undefined,
});
}else if(!p || j.startsWith(p + "/")) seen.add(j);
});
@ -368,10 +407,10 @@ function withSharedBranches(kind, own, hooks){
const seg = (p ? j.slice(p.length + 1) : j).split("/")[0];
if(!seg) return;
const full = p ? p + "/" + seg : seg;
const count = inScope(scope).filter(it=>{ const ij = join(split(it.folder)); return ij === full || ij.startsWith(full + "/"); }).length;
folderMap.set(seg, count);
const under = inScope(scope).filter(it=>{ const ij = join(split(it.folder)); return ij === full || ij.startsWith(full + "/"); });
folderMap.set(seg, { count: under.length, locked: under.length > 0 && under.some(it=>!it.canManage) });
});
const folders = [...folderMap].map(([name, count])=>({name, count}))
const folders = [...folderMap].map(([name, v])=>({name, count: v.count, locked: v.locked}))
.sort((a,b)=>a.name.localeCompare(b.name));
return { folders, items };
},
@ -406,6 +445,8 @@ function withSharedBranches(kind, own, hooks){
const from = join(srcPath.slice(1));
const to = join([...targetPath.slice(1), last]);
if(from === to) return null;
const under = inScope(scope).filter(it=>{ const j = join(split(it.folder)); return j === from || j.startsWith(from + "/"); });
if(under.some(it=>!it.canManage)) return T("shReadonly");
await api(`/shared/${kind}/folder`, { method:"PATCH", body:{ scope, from, to } });
[...pending[scope]].forEach(j=>{
if(j === from || j.startsWith(from + "/")){ pending[scope].delete(j); pending[scope].add(to + j.slice(from.length)); }
@ -454,6 +495,8 @@ function withSharedBranches(kind, own, hooks){
},
async renameItem(key, name){
if(typeof key !== "string" || !key.startsWith("sh:")) return own.renameItem(key, name);
const meta = cache.find(it=>"sh:"+it.id === key);
if(meta && !meta.canManage) return T("shReadonly");
try{
await api(`/shared/${kind}/${key.slice(3)}`, { method:"PATCH", body:{ name } });
await refresh();
@ -479,6 +522,7 @@ function withSharedBranches(kind, own, hooks){
if(srcShared && tgtShared){
const meta = cache.find(it=>"sh:"+it.id === key);
if(meta && scopeOf(dest[0]) === meta.scope){
if(!meta.canManage) return T("shReadonly");
await api(`/shared/${kind}/${id}`, { method:"PATCH", body:{ folder: join(dest.slice(1)) } });
pending[meta.scope].delete(join(dest.slice(1)));
await refresh();
@ -497,12 +541,42 @@ function withSharedBranches(kind, own, hooks){
},
async deleteItem(key){
if(typeof key !== "string" || !key.startsWith("sh:")) return own.deleteItem(key);
const meta = cache.find(it=>"sh:"+it.id === key);
if(meta && !meta.canManage) return T("shReadonly");
try{
await api(`/shared/${kind}/${key.slice(3)}`, { method:"DELETE" });
await refresh();
return null;
}catch(e){ return wrapErr(e); }
},
/* alleen-lezen aan/uit in de school-tak: op het geselecteerde item, of
zonder selectie op de map waar je nu in staat (alle items eronder) */
lockLabel(path){
if(!isShared(path) || scopeOf(path[0]) !== "school") return null;
const roles = [currentUser.role, ...(currentUser.extraRoles||[])];
if(!roles.includes("admin") && !roles.includes("super")) return null;
return "🔒 " + T("exLock");
},
async toggleLock(sel, path){
try{
if(sel && sel.type === "item" && String(sel.key).startsWith("sh:")){
const meta = cache.find(it=>"sh:"+it.id === sel.key);
if(!meta) return null;
await api(`/shared/${kind}/${meta.id}`, { method:"PATCH", body:{ readonly: !meta.readonly } });
await refresh();
return { ok: meta.readonly ? T("shUnlockedOk") : T("shLockedOk") };
}
const sub = path.slice(1);
if(!sub.length) return T("exSelectFirst");
const from = join(sub);
const under = inScope("school").filter(it=>{ const j = join(split(it.folder)); return j === from || j.startsWith(from + "/"); });
if(!under.length) return T("exEmpty");
const lock = under.some(it=>!it.readonly);
await api(`/shared/${kind}/folder`, { method:"PATCH", body:{ scope: "school", from, readonly: lock } });
await refresh();
return { ok: lock ? T("shLockedOk") : T("shUnlockedOk") };
}catch(e){ return wrapErr(e); }
},
async openItem(key){
if(typeof key !== "string" || !key.startsWith("sh:")) return own.openItem(key);
try{