Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5
6 changed files with 375 additions and 31 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.26-beta
|
||||
0.3.27-beta
|
||||
|
|
|
|||
|
|
@ -221,6 +221,8 @@
|
|||
.ex-sep{color:var(--muted); font-weight:800;}
|
||||
.ex-tools{display:flex; gap:8px; flex-wrap:wrap;}
|
||||
.ex-msg{min-height:18px; color:var(--red); font-size:13px; font-weight:700;}
|
||||
.ex-msg.ok{color:var(--green, #2c8c4b);}
|
||||
.ex-hint{font-size:11px; color:var(--muted); font-weight:600;}
|
||||
.ex-grid{
|
||||
display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr));
|
||||
gap:12px; margin-top:8px;
|
||||
|
|
|
|||
|
|
@ -1017,8 +1017,62 @@ function switchTo(f, b){
|
|||
const btnFolders = document.getElementById("btnFolders");
|
||||
btnFolders.addEventListener("click", ()=>{
|
||||
stashCurrent(); /* verse thumbnails, ook van het actieve bord */
|
||||
openExplorer(boardsExplorerAdapter());
|
||||
openExplorer(withSharedBranches("board", boardsExplorerAdapter(), boardsSharedHooks()));
|
||||
});
|
||||
/* koppelstuk voor de gedeelde bibliotheek (zie withSharedBranches in
|
||||
explorer.js): borden exporteren/importeren als {name, data}. Een kopie
|
||||
krijgt altijd een nieuw bord-id en verse wids, anders zouden toewijzingen
|
||||
en voortgang van verschillende gebruikers naar hetzelfde id verwijzen. */
|
||||
function freshBoardCopy(name, data){
|
||||
const d = data ? structuredClone(data) : null;
|
||||
if(d && Array.isArray(d.widgets)) d.widgets.forEach(w=>{ w.wid = genWidgetId(); });
|
||||
return { id: genBoardId(), name, data: d };
|
||||
}
|
||||
function boardsSharedHooks(){
|
||||
const entryAtPath = path => BS.folders.find(f=>joinBoardPath(splitBoardPath(f.name))===joinBoardPath(path));
|
||||
const ensureEntry = path => {
|
||||
let e = entryAtPath(path);
|
||||
if(!e){ e = { name: joinBoardPath(path), boards: [] }; BS.folders.push(e); }
|
||||
return e;
|
||||
};
|
||||
const boardData = hit => (hit.fi===BS.f && hit.bi===BS.b) ? serializeBoard() : (hit.bo.data || emptyBoardData());
|
||||
return {
|
||||
exportItem(id){
|
||||
const hit = findBoardEntry(id);
|
||||
return hit ? { name: hit.bo.name, data: boardData(hit) } : null;
|
||||
},
|
||||
exportFolder(path){
|
||||
const p = joinBoardPath(path);
|
||||
const out = [];
|
||||
BS.folders.forEach((f, fi)=>{
|
||||
const j = joinBoardPath(splitBoardPath(f.name));
|
||||
if(j === p || j.startsWith(p + "/")){
|
||||
const rel = j === p ? [] : j.slice(p.length + 1).split("/");
|
||||
f.boards.forEach((bo, bi)=>out.push({
|
||||
name: bo.name, rel,
|
||||
data: (fi===BS.f && bi===BS.b) ? serializeBoard() : (bo.data || emptyBoardData()),
|
||||
}));
|
||||
}
|
||||
});
|
||||
return out;
|
||||
},
|
||||
copyIn(item, destPath){
|
||||
if(!destPath.length) return T("exNoRootItems");
|
||||
ensureEntry(destPath).boards.push(freshBoardCopy(item.name, item.data));
|
||||
markBoardChange();
|
||||
return null;
|
||||
},
|
||||
openShared(item, relPath){
|
||||
const entry = ensureEntry(relPath.length ? relPath : [T("shGlobal")]);
|
||||
const copy = freshBoardCopy(item.name, item.data);
|
||||
entry.boards.push(copy);
|
||||
markBoardChange();
|
||||
const hit = findBoardEntry(copy.id);
|
||||
if(hit) switchTo(hit.fi, hit.bi);
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
function updateBoardsUI(){
|
||||
btnFolders.title = `${T("boardsNav")} · ${BS.folders[BS.f].name} / ${curSlot().name}`;
|
||||
fabBoardsBtn.title = `${BS.folders[BS.f].name} / ${curSlot().name}`;
|
||||
|
|
@ -1030,16 +1084,17 @@ function updateBoardsUI(){
|
|||
dus bestaande data en de server (findBoardById op bord-id) blijven werken */
|
||||
const splitBoardPath = n => n.split("/").map(s=>s.trim()).filter(Boolean);
|
||||
const joinBoardPath = p => p.join("/");
|
||||
function boardsExplorerAdapter(){
|
||||
const norm = f => joinBoardPath(splitBoardPath(f.name));
|
||||
const entryAt = path => BS.folders.find(f=>norm(f)===joinBoardPath(path));
|
||||
const findBoard = id => {
|
||||
function findBoardEntry(id){
|
||||
for(let fi=0; fi<BS.folders.length; fi++){
|
||||
const bi = BS.folders[fi].boards.findIndex(b=>b.id===id);
|
||||
if(bi >= 0) return { fi, bi, fo: BS.folders[fi], bo: BS.folders[fi].boards[bi] };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
function boardsExplorerAdapter(){
|
||||
const norm = f => joinBoardPath(splitBoardPath(f.name));
|
||||
const entryAt = path => BS.folders.find(f=>norm(f)===joinBoardPath(path));
|
||||
const findBoard = findBoardEntry;
|
||||
const activeId = ()=>curSlot().id;
|
||||
/* indices herstellen nadat entries verschoven/verwijderd zijn */
|
||||
const relocate = (id)=>{
|
||||
|
|
|
|||
|
|
@ -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.26-beta";
|
||||
const VERSION = "0.3.27-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
@ -85,6 +85,9 @@ const I18N = {
|
|||
exFolderNotEmpty:"Map is niet leeg - verplaats of verwijder eerst de inhoud.",
|
||||
exIntoSelf:"Een map kan niet in zichzelf.", exNoRootItems:"Borden horen in een map.",
|
||||
exLastBoard:"Het laatste bord kan niet weg.",
|
||||
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.",
|
||||
hudMin:"Menu inklappen/uitklappen", hudOrient:"Horizontaal of verticaal",
|
||||
wFull:"Maximaliseren", wUnfull:"Terugzetten",
|
||||
wZoom:"Inhoud groter of kleiner (voor slechtzienden)",
|
||||
|
|
@ -261,6 +264,9 @@ const I18N = {
|
|||
exFolderNotEmpty:"Folder is not empty - move or delete its contents first.",
|
||||
exIntoSelf:"A folder cannot go inside itself.", exNoRootItems:"Boards belong in a folder.",
|
||||
exLastBoard:"The last board cannot be deleted.",
|
||||
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.",
|
||||
hudMin:"Collapse/expand menu", hudOrient:"Horizontal or vertical",
|
||||
wFull:"Maximize", wUnfull:"Restore",
|
||||
wZoom:"Content bigger or smaller (low vision)",
|
||||
|
|
|
|||
|
|
@ -37,8 +37,10 @@
|
|||
let SEL = null; /* selectie: {type:"folder"|"item", key} */
|
||||
let onCloseCb = null;
|
||||
|
||||
window.openExplorer = function(adapter, onClose){
|
||||
window.openExplorer = async function(adapter, onClose){
|
||||
AD = adapter; PATH = []; SEL = null; onCloseCb = onClose || null;
|
||||
/* adapters met externe data (gedeelde bibliotheek) laden die eerst */
|
||||
if(AD.init){ try{ await AD.init(); }catch(e){} }
|
||||
render();
|
||||
msg("");
|
||||
wrap.classList.add("open");
|
||||
|
|
@ -51,7 +53,13 @@
|
|||
wrap.addEventListener("click", e=>{ if(e.target===wrap) closeExplorer(); });
|
||||
document.querySelector(".explorer-close").addEventListener("click", closeExplorer);
|
||||
|
||||
const msg = t => { msgEl.textContent = t || ""; };
|
||||
const msg = t => { msgEl.classList.remove("ok"); msgEl.textContent = t || ""; };
|
||||
const msgOk = t => { msgEl.classList.add("ok"); msgEl.textContent = t || ""; };
|
||||
/* mutaties geven null (ok), een foutstring, of {ok:"..."} (groene melding) */
|
||||
function showResult(res){
|
||||
if(res && res.ok) msgOk(res.ok);
|
||||
else if(res) msg(res);
|
||||
}
|
||||
const validName = (name)=>{
|
||||
const n = (name||"").trim();
|
||||
if(!n) return null;
|
||||
|
|
@ -124,21 +132,21 @@
|
|||
document.addEventListener("pointerup", up);
|
||||
});
|
||||
}
|
||||
function doMove(payload, dest){
|
||||
async function doMove(payload, dest){
|
||||
msg("");
|
||||
let err = null;
|
||||
let res = null;
|
||||
if(payload.type === "folder"){
|
||||
const src = [...PATH, payload.key];
|
||||
/* niet in zichzelf of een submap slepen */
|
||||
if(dest.length >= src.length && src.every((s,i)=>dest[i]===s)){ msg(T("exIntoSelf")); return; }
|
||||
err = AD.moveFolder(src, dest);
|
||||
res = await AD.moveFolder(src, dest);
|
||||
}else{
|
||||
if(!dest.length && !AD.allowRootItems){ msg(T("exNoRootItems")); return; }
|
||||
err = AD.moveItem(payload.key, dest);
|
||||
res = await AD.moveItem(payload.key, dest);
|
||||
}
|
||||
SEL = null;
|
||||
render();
|
||||
if(err) msg(err);
|
||||
showResult(res);
|
||||
}
|
||||
|
||||
function tileEl(cls){
|
||||
|
|
@ -191,17 +199,17 @@
|
|||
const holder = document.createElement("div");
|
||||
t.appendChild(holder);
|
||||
gridEl.prepend(t);
|
||||
inlineInput(holder, "", (n)=>{
|
||||
const err = AD.createFolder(PATH, n);
|
||||
inlineInput(holder, "", async (n)=>{
|
||||
const res = await AD.createFolder(PATH, n);
|
||||
render();
|
||||
if(err) msg(err);
|
||||
showResult(res);
|
||||
});
|
||||
});
|
||||
if(AD.createItem && (PATH.length || AD.allowRootItems)){
|
||||
tool("+ " + AD.newItemLabel(), ()=>{
|
||||
const err = AD.createItem(PATH);
|
||||
tool("+ " + AD.newItemLabel(), async ()=>{
|
||||
const res = await AD.createItem(PATH);
|
||||
render();
|
||||
if(err) msg(err);
|
||||
showResult(res);
|
||||
});
|
||||
}
|
||||
tool("✏ " + T("exRename"), ()=>{
|
||||
|
|
@ -209,24 +217,24 @@
|
|||
const t = gridEl.querySelector(".ex-tile.sel .ex-name");
|
||||
if(!t) return;
|
||||
const cur = SEL.type==="folder" ? SEL.key : t.textContent;
|
||||
inlineInput(t, cur, (n)=>{
|
||||
const err = SEL.type==="folder" ? AD.renameFolder(PATH, SEL.key, n) : AD.renameItem(SEL.key, n);
|
||||
inlineInput(t, cur, async (n)=>{
|
||||
const res = SEL.type==="folder" ? await AD.renameFolder(PATH, SEL.key, n) : await AD.renameItem(SEL.key, n);
|
||||
SEL = null;
|
||||
render();
|
||||
if(err) msg(err);
|
||||
showResult(res);
|
||||
});
|
||||
});
|
||||
const delBtn = tool("🗑 " + T("exDelete"), ()=>{
|
||||
const delBtn = tool("🗑 " + T("exDelete"), async ()=>{
|
||||
if(!SEL){ msg(T("exSelectFirst")); return; }
|
||||
if(!delBtn.classList.contains("danger")){
|
||||
delBtn.classList.add("danger");
|
||||
delBtn.textContent = "🗑 " + T("exConfirm");
|
||||
return;
|
||||
}
|
||||
const err = SEL.type==="folder" ? AD.deleteFolder(PATH, SEL.key) : AD.deleteItem(SEL.key);
|
||||
const res = SEL.type==="folder" ? await AD.deleteFolder(PATH, SEL.key) : await AD.deleteItem(SEL.key);
|
||||
SEL = null;
|
||||
render();
|
||||
if(err) msg(err);
|
||||
showResult(res);
|
||||
});
|
||||
|
||||
/* raster: eerst mappen, dan items */
|
||||
|
|
@ -256,8 +264,17 @@
|
|||
t.appendChild(ico);
|
||||
}
|
||||
t.appendChild(nameEl(it.name));
|
||||
if(it.hint){
|
||||
const hn = document.createElement("div");
|
||||
hn.className = "ex-hint"; hn.textContent = it.hint;
|
||||
t.appendChild(hn);
|
||||
}
|
||||
t.addEventListener("click", ()=>{ SEL = {type:"item", key:it.key}; markSel(t); });
|
||||
t.addEventListener("dblclick", ()=>{ AD.openItem(it.key); closeExplorer(); });
|
||||
t.addEventListener("dblclick", async ()=>{
|
||||
const res = await AD.openItem(it.key);
|
||||
if(typeof res === "string"){ render(); msg(res); return; }
|
||||
closeExplorer();
|
||||
});
|
||||
makeDraggable(t, {type:"item", key:it.key, movable: it.movable !== false});
|
||||
gridEl.appendChild(t);
|
||||
});
|
||||
|
|
@ -276,3 +293,222 @@
|
|||
if(db){ db.classList.remove("danger"); db.textContent = "🗑 " + T("exDelete"); }
|
||||
}
|
||||
})();
|
||||
|
||||
/* =========================================================
|
||||
Gedeelde bibliotheek in de verkenner: wikkelt een "eigen inhoud"-adapter
|
||||
en voegt in de root twee vaste takken toe: 🌐 Systeem (systeembreed,
|
||||
gepubliceerd door de systeemmanager) en 🏫 School (gedeeld binnen de
|
||||
eigen school). Slepen tussen de bomen = kopiëren:
|
||||
eigen item/map → gedeelde tak = publiceren (POST /shared)
|
||||
gedeeld item/map → eigen boom = kopiëren naar eigen collectie
|
||||
Binnen een gedeelde tak = echt verplaatsen (PATCH, server bewaakt rechten).
|
||||
hooks (per soort inhoud):
|
||||
exportItem(key) -> {name, data} | null
|
||||
exportFolder(path) -> [{name, rel:[...], data}] (alles onder het pad)
|
||||
copyIn(sharedItem, destPath) -> err|null (gedeeld item in eigen boom zetten)
|
||||
openShared(sharedItem, relPath)-> err|null (dubbelklik op gedeeld item)
|
||||
==========================================================*/
|
||||
function withSharedBranches(kind, own, hooks){
|
||||
/* gasten en leerlingen zien alleen de eigen boom */
|
||||
if(!currentUser || currentUser.role === "pupil") return own;
|
||||
const GLOBAL = "🌐 " + T("shGlobal");
|
||||
const SCHOOL = "🏫 " + T("shSchool");
|
||||
const branches = [GLOBAL];
|
||||
if(currentUser.schoolId != null) branches.push(SCHOOL);
|
||||
const scopeOf = seg => seg === GLOBAL ? "global" : "school";
|
||||
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
|
||||
const join = p => p.join("/");
|
||||
let cache = [];
|
||||
const pending = { global: new Set(), school: new Set() };
|
||||
const inScope = scope => cache.filter(it=>it.scope===scope);
|
||||
const isShared = path => path.length > 0 && branches.includes(path[0]);
|
||||
const refresh = async ()=>{ cache = (await api("/shared/"+kind)).items || []; };
|
||||
const wrapErr = e => e && e.message ? e.message : String(e);
|
||||
|
||||
async function publishOne(scope, folder, name, data){
|
||||
await api("/shared/"+kind, { method:"POST", body:{ name, folder, data, scope } });
|
||||
}
|
||||
async function fetchShared(id){
|
||||
return (await api(`/shared/${kind}/${id}`)).item;
|
||||
}
|
||||
|
||||
return {
|
||||
title: own.title,
|
||||
allowRootItems: own.allowRootItems,
|
||||
newItemLabel: own.newItemLabel,
|
||||
async init(){ try{ await refresh(); }catch(e){ cache = []; } },
|
||||
list(path){
|
||||
if(!isShared(path)){
|
||||
const r = own.list(path);
|
||||
if(path.length === 0){
|
||||
branches.forEach(b=>r.folders.push({ name:b, count: inScope(scopeOf(b)).length, fixed:true }));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
const scope = scopeOf(path[0]);
|
||||
const sub = path.slice(1);
|
||||
const p = join(sub);
|
||||
const folderMap = new Map();
|
||||
const items = [];
|
||||
const seen = new Set(pending[scope]);
|
||||
inScope(scope).forEach(it=>{
|
||||
const j = join(split(it.folder));
|
||||
if(j === p){
|
||||
items.push({
|
||||
key: "sh:"+it.id, name: it.name,
|
||||
icon: kind === "board" ? "▦" : "⚓",
|
||||
hint: it.ownerName || undefined,
|
||||
});
|
||||
}else if(!p || j.startsWith(p + "/")) seen.add(j);
|
||||
});
|
||||
seen.forEach(j=>{
|
||||
if(j === p) return;
|
||||
if(p && !j.startsWith(p + "/")) return;
|
||||
if(!p && !j) return;
|
||||
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 folders = [...folderMap].map(([name, count])=>({name, count}))
|
||||
.sort((a,b)=>a.name.localeCompare(b.name));
|
||||
return { folders, items };
|
||||
},
|
||||
createItem: own.createItem ? (path)=>{
|
||||
if(isShared(path)) return T("shNoCreateHere");
|
||||
return own.createItem(path);
|
||||
} : undefined,
|
||||
createFolder(path, name){
|
||||
if(!isShared(path)){
|
||||
if(path.length === 0 && branches.includes(name)) return T("exExists");
|
||||
return own.createFolder(path, name);
|
||||
}
|
||||
const scope = scopeOf(path[0]);
|
||||
if(this.list(path).folders.some(f=>f.name===name)) return T("exExists");
|
||||
pending[scope].add(join([...path.slice(1), name]));
|
||||
return null;
|
||||
},
|
||||
async renameFolder(path, oldName, newName){
|
||||
if(!isShared(path) && !(path.length === 0 && branches.includes(oldName)))
|
||||
return own.renameFolder(path, oldName, newName);
|
||||
return this.moveFolder([...path, oldName], path, newName);
|
||||
},
|
||||
async moveFolder(srcPath, targetPath, newName){
|
||||
const srcShared = isShared(srcPath), tgtShared = isShared(targetPath);
|
||||
if(srcPath.length === 1 && branches.includes(srcPath[0])) return T("exFixedFolder");
|
||||
if(!srcShared && !tgtShared) return own.moveFolder(srcPath, targetPath, newName);
|
||||
const last = newName || srcPath[srcPath.length-1];
|
||||
try{
|
||||
if(srcShared && tgtShared && scopeOf(srcPath[0]) === scopeOf(targetPath[0])){
|
||||
/* binnen dezelfde tak: echte verplaatsing via prefix-rewrite */
|
||||
const scope = scopeOf(srcPath[0]);
|
||||
const from = join(srcPath.slice(1));
|
||||
const to = join([...targetPath.slice(1), last]);
|
||||
if(from === to) return null;
|
||||
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)); }
|
||||
});
|
||||
await refresh();
|
||||
return null;
|
||||
}
|
||||
if(!srcShared && tgtShared){
|
||||
/* hele eigen map publiceren (kopie; het origineel blijft staan) */
|
||||
const scope = scopeOf(targetPath[0]);
|
||||
const base = [...targetPath.slice(1), last];
|
||||
const list = hooks.exportFolder(srcPath);
|
||||
for(const it of list) await publishOne(scope, join([...base, ...it.rel]), it.name, it.data);
|
||||
await refresh();
|
||||
return { ok: T("shPublished") };
|
||||
}
|
||||
/* gedeelde map → eigen boom (of andere tak): itemsgewijs kopiëren */
|
||||
const scope = scopeOf(srcPath[0]);
|
||||
const from = join(srcPath.slice(1));
|
||||
const under = inScope(scope).filter(it=>{ const j = join(split(it.folder)); return j === from || j.startsWith(from + "/"); });
|
||||
for(const it of under){
|
||||
const full = await fetchShared(it.id);
|
||||
const rel = split(it.folder).slice(srcPath.length - 1);
|
||||
if(tgtShared){
|
||||
await publishOne(scopeOf(targetPath[0]), join([...targetPath.slice(1), last, ...rel]), full.name, full.data);
|
||||
}else{
|
||||
const err = hooks.copyIn(full, [...targetPath, last, ...rel]);
|
||||
if(err) return err;
|
||||
}
|
||||
}
|
||||
if(tgtShared) await refresh();
|
||||
return { ok: tgtShared ? T("shPublished") : T("shCopied") };
|
||||
}catch(e){ return wrapErr(e); }
|
||||
},
|
||||
async deleteFolder(path, name){
|
||||
if(!isShared(path)){
|
||||
if(path.length === 0 && branches.includes(name)) return T("exFixedFolder");
|
||||
return own.deleteFolder(path, name);
|
||||
}
|
||||
const scope = scopeOf(path[0]);
|
||||
const full = join([...path.slice(1), name]);
|
||||
if(inScope(scope).some(it=>{ const j = join(split(it.folder)); return j === full || j.startsWith(full + "/"); }))
|
||||
return T("exFolderNotEmpty");
|
||||
[...pending[scope]].forEach(j=>{ if(j === full || j.startsWith(full + "/")) pending[scope].delete(j); });
|
||||
return null;
|
||||
},
|
||||
async renameItem(key, name){
|
||||
if(typeof key !== "string" || !key.startsWith("sh:")) return own.renameItem(key, name);
|
||||
try{
|
||||
await api(`/shared/${kind}/${key.slice(3)}`, { method:"PATCH", body:{ name } });
|
||||
await refresh();
|
||||
return null;
|
||||
}catch(e){ return wrapErr(e); }
|
||||
},
|
||||
async moveItem(key, dest){
|
||||
const srcShared = typeof key === "string" && key.startsWith("sh:");
|
||||
const tgtShared = isShared(dest);
|
||||
try{
|
||||
if(!srcShared && !tgtShared) return own.moveItem(key, dest);
|
||||
if(!srcShared && tgtShared){
|
||||
/* eigen item publiceren (kopie) */
|
||||
const exp = hooks.exportItem(key);
|
||||
if(!exp) return null;
|
||||
const scope = scopeOf(dest[0]);
|
||||
await publishOne(scope, join(dest.slice(1)), exp.name, exp.data);
|
||||
pending[scope].delete(join(dest.slice(1)));
|
||||
await refresh();
|
||||
return { ok: T("shPublished") };
|
||||
}
|
||||
const id = key.slice(3);
|
||||
if(srcShared && tgtShared){
|
||||
const meta = cache.find(it=>"sh:"+it.id === key);
|
||||
if(meta && scopeOf(dest[0]) === meta.scope){
|
||||
await api(`/shared/${kind}/${id}`, { method:"PATCH", body:{ folder: join(dest.slice(1)) } });
|
||||
pending[meta.scope].delete(join(dest.slice(1)));
|
||||
await refresh();
|
||||
return null;
|
||||
}
|
||||
const full = await fetchShared(id);
|
||||
await publishOne(scopeOf(dest[0]), join(dest.slice(1)), full.name, full.data);
|
||||
await refresh();
|
||||
return { ok: T("shPublished") };
|
||||
}
|
||||
/* gedeeld item → eigen boom: kopiëren */
|
||||
const full = await fetchShared(id);
|
||||
const err = hooks.copyIn(full, dest);
|
||||
return err || { ok: T("shCopied") };
|
||||
}catch(e){ return wrapErr(e); }
|
||||
},
|
||||
async deleteItem(key){
|
||||
if(typeof key !== "string" || !key.startsWith("sh:")) return own.deleteItem(key);
|
||||
try{
|
||||
await api(`/shared/${kind}/${key.slice(3)}`, { method:"DELETE" });
|
||||
await refresh();
|
||||
return null;
|
||||
}catch(e){ return wrapErr(e); }
|
||||
},
|
||||
async openItem(key){
|
||||
if(typeof key !== "string" || !key.startsWith("sh:")) return own.openItem(key);
|
||||
try{
|
||||
const full = await fetchShared(key.slice(3));
|
||||
return hooks.openShared(full, split(full.folder));
|
||||
}catch(e){ return wrapErr(e); }
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,7 +344,52 @@ function mountAnchor(root, initState, opts){
|
|||
saveBtn.textContent = "✓";
|
||||
setTimeout(()=>{ saveBtn.textContent = orig; }, 1200);
|
||||
});
|
||||
libBtn.addEventListener("click", ()=>openExplorer(anchorLibAdapter()));
|
||||
/* koppelstuk voor de gedeelde bibliotheek: diagrammen als {name, data} */
|
||||
const anchorSharedHooks = ()=>{
|
||||
const split = n => (n||"").split("/").map(s=>s.trim()).filter(Boolean);
|
||||
const join = p => p.join("/");
|
||||
return {
|
||||
exportItem(i){
|
||||
const it = ANCHOR_LIB[i];
|
||||
return it ? { name: it.name, data: { cells: structuredClone(it.cells||{}), pics: structuredClone(it.pics||{}) } } : null;
|
||||
},
|
||||
exportFolder(path){
|
||||
const p = join(path);
|
||||
const out = [];
|
||||
ANCHOR_LIB.forEach(it=>{
|
||||
const j = join(split(it.folder));
|
||||
if(j === p || j.startsWith(p + "/")){
|
||||
const rel = j === p ? [] : j.slice(p.length + 1).split("/");
|
||||
out.push({ name: it.name, rel, data: { cells: structuredClone(it.cells||{}), pics: structuredClone(it.pics||{}) } });
|
||||
}
|
||||
});
|
||||
return out;
|
||||
},
|
||||
copyIn(item, destPath){
|
||||
const entry = {
|
||||
id: "al-"+Math.random().toString(36).slice(2,10),
|
||||
name: item.name,
|
||||
cells: structuredClone(item.data.cells || {}),
|
||||
pics: structuredClone(item.data.pics || {}),
|
||||
};
|
||||
if(destPath.length) entry.folder = join(destPath);
|
||||
ANCHOR_LIB.push(entry);
|
||||
scheduleSave("anchors");
|
||||
return null;
|
||||
},
|
||||
openShared(item){
|
||||
S.cells = structuredClone(item.data.cells || {});
|
||||
S.pics = structuredClone(item.data.pics || {});
|
||||
S.pan = {x:0, y:0};
|
||||
syncSource();
|
||||
render();
|
||||
persistWords();
|
||||
markBoardChange();
|
||||
return null;
|
||||
},
|
||||
};
|
||||
};
|
||||
libBtn.addEventListener("click", ()=>openExplorer(withSharedBranches("anchor", anchorLibAdapter(), anchorSharedHooks())));
|
||||
}
|
||||
|
||||
new ResizeObserver(render).observe(stage);
|
||||
|
|
|
|||
Loading…
Reference in a new issue