Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
3 changed files with 95 additions and 17 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.28-beta
|
||||
0.3.29-beta
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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 ná 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{
|
||||
|
|
|
|||
Loading…
Reference in a new issue