/* teach - doorzoekbare afbeeldingscatalogus voor whiteboard en plaatjeskiezers.
Bestanden staan op de server; op het bord bewaren we alleen een stabiele URL. */
"use strict";
let IMAGE_CATALOG = {images:[],themes:[],folders:[],quota:{},uploadScope:"personal"};
let imageCatalogWrap = null, imageCatalogSelect = null, imageCatalogDeleteArmed = false;
const imageBytes = value=>{
const n=Number(value||0);
if(n<1024*1024)return Math.max(0,Math.round(n/1024))+" KB";
return (n/(1024*1024)).toLocaleString(LANG,{maximumFractionDigits:1})+" MB";
};
const imageScopeName = scope=>T(scope==="global"?"imgScopeGlobal":scope==="school"?"imgScopeSchool":"imgScopePersonal");
const imageThemeName = theme=>LANG==="nl"?theme.nameNl:theme.nameEn;
const imageFileName = file=>(file.name||T("imgUntitled")).replace(/\.[^.]+$/,"").slice(0,80);
async function uploadImageToCatalog(file, options={}){
if(!currentUser)throw new Error(T("imgLoginRequired"));
if(!file||!file.size)throw new Error(T("imgInvalidFile"));
if(file.size>50*1024*1024)throw new Error(T("imgFileTooLarge"));
const params=new URLSearchParams({
name:(options.name||imageFileName(file)).slice(0,80),
folder:options.folder||"",
themes:(options.themeIds||[]).join(",")
});
const response=await fetch("/api/images?"+params,{
method:"POST",credentials:"same-origin",
headers:{"Content-Type":file.type||"application/octet-stream","x-file-name":encodeURIComponent(file.name||"")},
body:file
});
const json=await response.json().catch(()=>({}));
if(!response.ok)throw new Error(json.error||T("imgUploadFailed"));
return json.image;
}
async function loadImageCatalog(){
IMAGE_CATALOG=await api("/images");
refreshWordPictureCatalog();
return IMAGE_CATALOG;
}
function refreshWordPictureCatalog(){
const wordCatalog=languagePictureCatalog(LANG);
IMAGE_CATALOG.images=(IMAGE_CATALOG.images||[]).filter(image=>!image.wordPicture);
IMAGE_CATALOG.themes=(IMAGE_CATALOG.themes||[]).filter(theme=>!theme.wordPicture);
IMAGE_CATALOG.images=wordCatalog.images.concat(IMAGE_CATALOG.images);
IMAGE_CATALOG.themes=wordCatalog.themes.concat(IMAGE_CATALOG.themes);
}
function ensureImageCatalog(){
if(imageCatalogWrap)return imageCatalogWrap;
const wrap=document.createElement("div");
wrap.id="imageCatalogWrap";wrap.className="modalwrap";wrap.setAttribute("role","dialog");wrap.setAttribute("aria-modal","true");
wrap.innerHTML=`
`;
document.body.append(wrap);imageCatalogWrap=wrap;
const q=s=>wrap.querySelector(s);
q(".ic-close").onclick=closeImageCatalog;
wrap.onclick=e=>{if(e.target===wrap)closeImageCatalog()};
q(".ic-search").oninput=renderImageCatalogGrid;
q(".ic-theme-filter").onchange=()=>{renderImageCatalogGrid();renderLanguageThemeChips()};
q(".ic-scope-filter").onchange=renderImageCatalogGrid;
q(".ic-folder-filter").onchange=renderImageCatalogGrid;
q(".ic-folders").onclick=openImageFolderExplorer;
q(".ic-cancel").onclick=()=>{q(".ic-editor").hidden=true;imageCatalogDeleteArmed=false};
q(".ic-save").onclick=saveCatalogImage;
q(".ic-delete").onclick=deleteCatalogImage;
q(".ic-theme-new button").onclick=createCatalogTheme;
q(".ic-upload input").onchange=async e=>{
const files=[...e.target.files];e.target.value="";
if(!files.length)return;
const folder=q(".ic-upload-folder").value;
const themeIds=[...q(".ic-upload-themes").selectedOptions].map(o=>+o.value);
setImageCatalogStatus(T("imgUploading"));
try{
for(const file of files)await uploadImageToCatalog(file,{folder,themeIds});
await loadImageCatalog();renderImageCatalog();setImageCatalogStatus(T("imgUploaded"),true);
}catch(err){setImageCatalogStatus(err.message)}
};
return wrap;
}
function setImageCatalogStatus(text,ok=false){
if(!imageCatalogWrap)return;
const status=imageCatalogWrap.querySelector(".ic-status");
status.textContent=text||"";status.classList.toggle("ok",!!ok);
}
function closeImageCatalog(){
if(imageCatalogWrap)imageCatalogWrap.classList.remove("open");
imageCatalogSelect=null;imageCatalogDeleteArmed=false;
}
function catalogOwnFolders(){
const scope=IMAGE_CATALOG.uploadScope;
const paths=new Set([""]);
IMAGE_CATALOG.folders.filter(f=>f.scope===scope&&f.canManage).forEach(f=>paths.add(f.path));
IMAGE_CATALOG.images.filter(i=>i.scope===scope&&i.canManage&&i.folder).forEach(i=>paths.add(i.folder));
return [...paths].sort((a,b)=>a.localeCompare(b,LANG));
}
function fillImageSelect(select,items,value,label){
select.innerHTML="";
if(label!=null)select.append(new Option(label,""));
items.forEach(item=>select.append(new Option(item.label,item.value)));
if([...select.options].some(o=>o.value===String(value)))select.value=String(value);
}
function fillThemeSelect(select,selected=[],includeAll=false,asset=null){
const current=new Set((selected||[]).map(Number));select.innerHTML="";
if(includeAll)select.append(new Option(T("imgAllThemes"),""));
IMAGE_CATALOG.themes.filter(theme=>(includeAll||!theme.filterOnly)&&(!asset||theme.scope==="global"
||(asset.scope==="school"&&theme.scope==="school")
||(asset.scope==="personal"&&theme.scope==="personal")))
.forEach(theme=>{
const option=new Option(imageThemeName(theme),theme.id);
option.selected=current.has(theme.id);select.append(option);
});
}
function renderLanguageThemeChips(){
if(!imageCatalogWrap)return;
const host=imageCatalogWrap.querySelector(".ic-word-themes");
const select=imageCatalogWrap.querySelector(".ic-theme-filter");
const themes=IMAGE_CATALOG.themes.filter(theme=>theme.languageBuiltin);
host.innerHTML="";host.hidden=!themes.length;
if(!themes.length)return;
const label=document.createElement("strong");label.textContent=T("imgWordThemes");host.append(label);
themes.forEach(theme=>{
const button=document.createElement("button");button.type="button";
button.textContent=(theme.icon||"🖼️")+" "+imageThemeName(theme);
button.classList.toggle("active",select.value===String(theme.id));
button.onclick=()=>{
select.value=select.value===String(theme.id)?"":String(theme.id);
renderImageCatalogGrid();renderLanguageThemeChips();
};
host.append(button);
});
}
function applyImageCatalogLabels(){
const q=s=>imageCatalogWrap.querySelector(s);
q(".ic-title").textContent=T("imgCatalog");
q(".ic-close").title=T("close");
q(".ic-search").placeholder=T("imgSearch");
q(".ic-folders").textContent="🗂 "+T("imgFolders");
q(".ic-add summary").textContent="+ "+T("imgUpload");
q(".ic-upload span").textContent=T("imgChooseFiles");
q(".ic-upload-folder-label").textContent=T("imgFolder");
q(".ic-upload-themes-label").textContent=T("imgThemes");
q(".ic-theme-new input").placeholder=T("imgNewTheme");
q(".ic-theme-new button").textContent=T("imgAddTheme");
q(".ic-editor h3").textContent=T("imgEdit");
q(".ic-name-label").textContent=T("imgName");
q(".ic-folder-label").textContent=T("imgFolder");
q(".ic-themes-label").textContent=T("imgThemes");
q(".ic-save").textContent=T("save");
q(".ic-delete").textContent=T("exDelete");
q(".ic-cancel").textContent=T("cancel");
}
function renderImageCatalog(){
const q=s=>imageCatalogWrap.querySelector(s);
refreshWordPictureCatalog();
applyImageCatalogLabels();
const themeValue=q(".ic-theme-filter").value,scopeValue=q(".ic-scope-filter").value,folderValue=q(".ic-folder-filter").value;
fillThemeSelect(q(".ic-theme-filter"),[],true);
fillImageSelect(q(".ic-scope-filter"),[
{value:"global",label:imageScopeName("global")},
{value:"school",label:imageScopeName("school")},
{value:"personal",label:imageScopeName("personal")}
],scopeValue,T("imgAllScopes"));
const allFolders=[...new Set(IMAGE_CATALOG.images.map(i=>i.folder).concat(IMAGE_CATALOG.folders.map(f=>f.path)).filter(Boolean))]
.sort((a,b)=>a.localeCompare(b,LANG)).map(path=>({value:path,label:path}));
fillImageSelect(q(".ic-folder-filter"),allFolders,folderValue,T("imgAllFolders"));
if(themeValue&&[...q(".ic-theme-filter").options].some(o=>o.value===themeValue))q(".ic-theme-filter").value=themeValue;
const ownFolders=catalogOwnFolders().map(path=>({value:path,label:path||T("imgFolderRoot")}));
fillImageSelect(q(".ic-upload-folder"),ownFolders,q(".ic-upload-folder").value);
fillThemeSelect(q(".ic-upload-themes"),[...q(".ic-upload-themes").selectedOptions].map(o=>+o.value),false,{scope:IMAGE_CATALOG.uploadScope});
renderLanguageThemeChips();
const quota=IMAGE_CATALOG.quota||{},limit=quota.limitBytes;
let text=T("imgQuotaUsed").replace("{used}",imageBytes(quota.usedBytes));
text+=" · "+(limit==null?T("imgUnlimited"):T("imgQuotaOf").replace("{limit}",imageBytes(limit)));
if(quota.schoolLimitBytes!=null)text+=" · "+T("imgSchoolQuota").replace("{used}",imageBytes(quota.schoolUsedBytes)).replace("{limit}",imageBytes(quota.schoolLimitBytes));
q(".ic-quota").textContent=text;
renderImageCatalogGrid();
}
function renderImageCatalogGrid(){
if(!imageCatalogWrap)return;
const q=s=>imageCatalogWrap.querySelector(s),grid=q(".ic-grid");
const search=q(".ic-search").value.trim().toLowerCase(),theme=+q(".ic-theme-filter").value||null;
const scope=q(".ic-scope-filter").value,folder=q(".ic-folder-filter").value;
const themeNames=new Map(IMAGE_CATALOG.themes.map(t=>[t.id,imageThemeName(t)]));
const images=IMAGE_CATALOG.images.filter(image=>{
const hay=[image.name,image.folder,...(image.tags||[]),...image.themes.map(id=>themeNames.get(id)||"")].join(" ").toLowerCase();
return(!search||hay.includes(search))&&(!theme||image.themes.includes(theme))
&&(!scope||image.scope===scope)&&(!folder||image.folder===folder||image.folder.startsWith(folder+"/"));
});
grid.innerHTML="";
images.forEach(image=>{
const card=document.createElement("article");card.className="ic-card";
const choose=document.createElement("button");choose.type="button";choose.className="ic-choose";
const img=document.createElement("img");img.src=image.src;img.alt=image.name;img.loading="lazy";
const name=document.createElement("strong");name.textContent=image.name;
const meta=document.createElement("span");meta.textContent=[imageScopeName(image.scope),image.folder].filter(Boolean).join(" · ");
choose.append(img,name,meta);choose.onclick=()=>{if(imageCatalogSelect){imageCatalogSelect(image.src,image);closeImageCatalog()}else if(image.canManage)openCatalogImageEditor(image)};
card.append(choose);
if(image.canManage){
const edit=document.createElement("button");edit.type="button";edit.className="ic-card-edit";edit.textContent="✏";
edit.title=T("imgEdit");edit.onclick=()=>openCatalogImageEditor(image);card.append(edit);
}
grid.append(card);
});
if(!images.length){const empty=document.createElement("p");empty.className="gallery-empty";empty.textContent=T("imgNoResults");grid.append(empty)}
}
function openCatalogImageEditor(image){
const q=s=>imageCatalogWrap.querySelector(s),editor=q(".ic-editor");
editor.hidden=false;editor.dataset.id=image.id;q(".ic-edit-name").value=image.name;q(".ic-edit-folder").value=image.folder;
fillThemeSelect(q(".ic-edit-themes"),image.themes,false,image);
q(".ic-delete").hidden=!image.canDelete;q(".ic-delete").textContent=T("exDelete");
imageCatalogDeleteArmed=false;editor.scrollIntoView({block:"nearest"});
}
async function saveCatalogImage(){
const q=s=>imageCatalogWrap.querySelector(s),editor=q(".ic-editor"),id=+editor.dataset.id;
const themeIds=[...q(".ic-edit-themes").selectedOptions].map(o=>+o.value);
try{
await api("/images/"+id,{method:"PATCH",body:{name:q(".ic-edit-name").value,folder:q(".ic-edit-folder").value,themeIds}});
await loadImageCatalog();editor.hidden=true;renderImageCatalog();setImageCatalogStatus(T("imgSaved"),true);
}catch(err){setImageCatalogStatus(err.message)}
}
async function deleteCatalogImage(){
const q=s=>imageCatalogWrap.querySelector(s),button=q(".ic-delete");
if(!imageCatalogDeleteArmed){imageCatalogDeleteArmed=true;button.textContent="⚠ "+T("exConfirm");return}
try{
await api("/images/"+q(".ic-editor").dataset.id,{method:"DELETE"});
await loadImageCatalog();q(".ic-editor").hidden=true;renderImageCatalog();setImageCatalogStatus(T("imgDeleted"),true);
}catch(err){setImageCatalogStatus(err.message)}
imageCatalogDeleteArmed=false;
}
async function createCatalogTheme(){
const input=imageCatalogWrap.querySelector(".ic-theme-new input"),name=input.value.trim();
if(!name)return;
try{
await api("/images/themes",{method:"POST",body:{name}});
input.value="";await loadImageCatalog();renderImageCatalog();setImageCatalogStatus(T("imgThemeAdded"),true);
}catch(err){setImageCatalogStatus(err.message)}
}
function imageScopeBranches(){
const scopes=["global"];
if(currentUser&¤tUser.schoolId!=null)scopes.push("school");
scopes.push("personal");
return scopes.map(scope=>({scope,name:(scope==="global"?"🌐 ":scope==="school"?"🏫 ":"👤 ")+imageScopeName(scope)}));
}
function openImageFolderExplorer(){
const branches=imageScopeBranches(),branchFor=path=>branches.find(b=>b.name===path[0]);
const join=path=>path.join("/");
const sameScope=(item,branch)=>item.scope===branch.scope;
const adapter={
title:()=>T("imgFolders"),allowRootItems:false,
list(path){
if(!path.length)return{folders:branches.map(b=>({name:b.name,count:IMAGE_CATALOG.images.filter(i=>sameScope(i,b)).length,fixed:true})),items:[]};
const branch=branchFor(path);if(!branch)return{folders:[],items:[]};
const rel=join(path.slice(1)),prefix=rel?rel+"/":"",folderNames=new Map();
const paths=IMAGE_CATALOG.folders.filter(f=>sameScope(f,branch)).map(f=>f.path)
.concat(IMAGE_CATALOG.images.filter(i=>sameScope(i,branch)).map(i=>i.folder)).filter(Boolean);
paths.forEach(full=>{if(rel&&full!==rel&&!full.startsWith(prefix))return;const rest=rel?full.slice(prefix.length):full;if(!rest)return;const seg=rest.split("/")[0],child=prefix+seg;folderNames.set(seg,IMAGE_CATALOG.images.filter(i=>sameScope(i,branch)&&(i.folder===child||i.folder.startsWith(child+"/"))).length)});
const items=IMAGE_CATALOG.images.filter(i=>sameScope(i,branch)&&i.folder===rel).map(image=>({
key:image.id,name:image.name,movable:image.canManage,deletable:image.canDelete,
tile(){const im=document.createElement("img");im.className="ic-explorer-thumb";im.src=image.src;im.alt="";im.loading="lazy";return im}
}));
return{folders:[...folderNames].map(([name,count])=>({name,count})),items};
},
async createFolder(path,name){
const branch=branchFor(path);if(!branch||branch.scope!==IMAGE_CATALOG.uploadScope)return T("imgNoManage");
try{await api("/images/folders",{method:"POST",body:{path:join([...path.slice(1),name])}});await loadImageCatalog();return null}catch(err){return err.message}
},
async renameFolder(path,oldName,newName){return this.moveFolder([...path,oldName],path,newName)},
async moveFolder(srcPath,targetPath,newName){
const source=branchFor(srcPath),target=branchFor(targetPath);
if(!source||!target||source.scope!==target.scope)return T("exWrongBranch");
const from=join(srcPath.slice(1)),folder=IMAGE_CATALOG.folders.find(f=>f.scope===source.scope&&f.path===from);
if(!folder||!folder.canManage)return T("imgNoManage");
const to=join([...targetPath.slice(1),newName||srcPath.at(-1)]);
try{await api("/images/folders/"+folder.id,{method:"PATCH",body:{path:to}});await loadImageCatalog();return null}catch(err){return err.message}
},
async deleteFolder(path,name){
const branch=branchFor(path),full=join([...path.slice(1),name]);
const folder=branch&&IMAGE_CATALOG.folders.find(f=>f.scope===branch.scope&&f.path===full);
if(!folder||!folder.canManage)return T("imgNoManage");
try{await api("/images/folders/"+folder.id,{method:"DELETE"});await loadImageCatalog();return null}catch(err){return err.message}
},
async renameItem(id,name){
try{await api("/images/"+id,{method:"PATCH",body:{name}});await loadImageCatalog();return null}catch(err){return err.message}
},
async moveItem(id,targetPath){
const image=IMAGE_CATALOG.images.find(i=>i.id===+id),branch=branchFor(targetPath);
if(!image||!branch||image.scope!==branch.scope||!image.canManage)return T("imgNoManage");
try{await api("/images/"+id,{method:"PATCH",body:{folder:join(targetPath.slice(1))}});await loadImageCatalog();return null}catch(err){return err.message}
},
async deleteItem(id){
const image=IMAGE_CATALOG.images.find(i=>i.id===+id);if(!image?.canDelete)return T("imgNoManage");
try{await api("/images/"+id,{method:"DELETE"});await loadImageCatalog();return null}catch(err){return err.message}
},
openItem(id){
const image=IMAGE_CATALOG.images.find(i=>i.id===+id);if(!image)return T("imgNoResults");
if(imageCatalogSelect)imageCatalogSelect(image.src,image);closeImageCatalog();return null;
}
};
imageCatalogWrap.style.visibility="hidden";
openExplorer(adapter,()=>{imageCatalogWrap.style.visibility="";if(imageCatalogWrap.classList.contains("open"))renderImageCatalog()});
}
async function openImageCatalog(onSelect){
if(!currentUser){alert(T("imgLoginRequired"));return}
const wrap=ensureImageCatalog();imageCatalogSelect=onSelect||null;setImageCatalogStatus(T("imgLoading"));
wrap.classList.add("open");
try{await loadImageCatalog();renderImageCatalog();setImageCatalogStatus("")}
catch(err){setImageCatalogStatus(err.message)}
}
/* Systeembeheer: limieten per gebruiker en school. Wordt door admin.js als
extra Instellingen-tab aangeroepen. */
/* Opslag-tab: eerst een school kiezen (of "Systeem" voor school-loze accounts),
daarna één groepskaart met de schoolquota in de kop en de gebruikers van die
school als compacte subrijen. Bewerkvelden zijn verborgen tot "Bewerken".
Keuze en open-status leven op moduleniveau: de save-handler en admin-render()
herbouwen het paneel en mogen niets vergeten. */
let imageQuotaSchool = "";
const imageQuotaOpen = new Set();
let imageQuotaFilter = "";
function renderImageStoragePanel(panel){
panel.innerHTML="";
const status=document.createElement("div");status.className="am-msg";panel.append(status);
const host=document.createElement("div");host.className="img-quota-admin";host.textContent=T("imgLoading");panel.append(host);
const fmt=value=>value==null?T("imgUnlimited"):imageBytes(value);
/* het bewerkblok (limiet-keuze + opslaan) achter de Bewerken-knop per kaart/rij */
const editorTools=(item,type)=>{
const wrap=document.createElement("div");wrap.className="am-inline";
const mode=document.createElement("select");mode.className="am-sel";
const defaultLabel=type==="users"&&item.defaultBytes!=null
? T("imgQuotaDefaultOf").replace("{limit}",imageBytes(item.defaultBytes))
: T("imgQuotaDefault");
[[defaultLabel,"default"],[T("imgQuotaCustom"),"custom"],[T("imgUnlimited"),"unlimited"]]
.forEach(([label,v])=>mode.add(new Option(label,v)));
mode.value=item.overrideBytes==null?"default":item.overrideBytes===-1?"unlimited":"custom";
const amount=document.createElement("input");amount.className="am-inp";amount.type="number";amount.min="0";amount.max=String(10*1024);amount.step="10";
amount.value=item.overrideBytes!=null&&item.overrideBytes>=0?Math.round(item.overrideBytes/(1024*1024)):"";
amount.placeholder=T("imgQuotaMb");amount.hidden=mode.value!=="custom";mode.onchange=()=>amount.hidden=mode.value!=="custom";
const save=document.createElement("button");save.className="am-btn";save.type="button";save.textContent=T("save");
save.onclick=async()=>{
const limitBytes=mode.value==="default"?null:mode.value==="unlimited"?-1:Math.round(Number(amount.value)*1024*1024);
if(mode.value==="custom"&&(!Number.isFinite(limitBytes)||limitBytes<0)){status.textContent=T("imgQuotaInvalid");return}
try{await api("/images/admin/"+type+"/"+item.id+"/quota",{method:"PATCH",body:{limitBytes}});renderImageStoragePanel(panel)}
catch(err){status.textContent=err.message}
};
wrap.append(mode,amount,save);
return wrap;
};
const usage=item=>T("imgQuotaUsage").replace("{used}",imageBytes(item.usedBytes)).replace("{limit}",fmt(item.limitBytes));
/* compacte gebruikersrij binnen de kaart: naam + verbruik, Bewerken klapt uit */
const userSub=u=>{
const row=document.createElement("div");row.className="am-row am-card-sub";
const nm=document.createElement("span");nm.className="am-name";nm.textContent=u.displayName;
const use=document.createElement("span");use.className="am-role";use.textContent=usage(u);
row.append(nm,use);
const tools=document.createElement("div");tools.className="am-row-tools";
tools.appendChild(editorTools(u,"users"));
const btn=document.createElement("button");btn.className="am-edit";btn.type="button";btn.textContent=T("amEdit");
const key="u:"+u.id;
const sync=()=>{const open=imageQuotaOpen.has(key);tools.style.display=open?"":"none";btn.classList.toggle("on",open)};
btn.addEventListener("click",()=>{imageQuotaOpen.has(key)?imageQuotaOpen.delete(key):imageQuotaOpen.add(key);sync()});
sync();
row.append(btn,tools);
return row;
};
(async()=>{
try{
const data=await api("/images/admin/quotas");host.innerHTML="";
/* stap 1: kies een school (of Systeem) - geen eindeloze platte lijsten */
const pick=document.createElement("select");pick.className="am-sel img-quota-pick";
pick.append(new Option(T("imgPickSchool"),""));
data.schools.forEach(s=>pick.append(new Option(s.name,String(s.id))));
pick.append(new Option(T("imgSystemScope"),"none"));
pick.value=imageQuotaSchool;
if(pick.value!==imageQuotaSchool) imageQuotaSchool=""; /* school verdwenen */
pick.onchange=()=>{imageQuotaSchool=pick.value;imageQuotaFilter="";if(pick.value)imageQuotaOpen.add("school:"+pick.value);renderImageStoragePanel(panel)};
host.append(pick);
if(!imageQuotaSchool){
const hint=document.createElement("div");hint.className="guestnote";hint.textContent=T("amPickSchoolHint");
host.append(hint);
return;
}
const system=imageQuotaSchool==="none";
const school=system?null:data.schools.find(s=>String(s.id)===imageQuotaSchool);
let users=data.users.filter(u=>system?u.schoolId==null:Number(u.schoolId)===Number(imageQuotaSchool));
/* zoeken binnen de gekozen school */
const filter=document.createElement("input");filter.className="am-inp";filter.type="search";
filter.placeholder=T("imgSearchUsers");filter.value=imageQuotaFilter;
filter.oninput=()=>{imageQuotaFilter=filter.value;draw()};
host.append(filter);
const cardHost=document.createElement("div");
host.append(cardHost);
const draw=()=>{
cardHost.innerHTML="";
const term=imageQuotaFilter.trim().toLowerCase();
const shown=users.filter(u=>(u.displayName+" "+u.username).toLowerCase().includes(term));
const card=amGroupCard({
icon:"🏫",
title:system?T("imgSystemScope"):school.name,
summary:(school?usage(school)+" · ":"")+shown.length+" "+T("imgUsers").toLowerCase(),
key:"school:"+imageQuotaSchool,
openSet:imageQuotaOpen, editSet:imageQuotaOpen,
edit:school?{key:"s:"+school.id,tools:editorTools(school,"schools")}:null,
subs:shown.map(userSub),
});
cardHost.append(card);
};
draw();
}catch(err){host.textContent="";status.textContent=err.message}
})();
}
const imageToolbarButton=document.getElementById("btnImages");
if(imageToolbarButton)imageToolbarButton.addEventListener("click",()=>openImageCatalog());
function refreshImageToolbar(){if(imageToolbarButton)imageToolbarButton.title=T("imgCatalog")}
document.addEventListener("langchange",()=>{refreshImageToolbar();if(imageCatalogWrap?.classList.contains("open"))renderImageCatalog()});
refreshImageToolbar();