All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 27s
360 lines
22 KiB
JavaScript
360 lines
22 KiB
JavaScript
/* 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");
|
||
return IMAGE_CATALOG;
|
||
}
|
||
|
||
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=`
|
||
<div class="modal image-catalog-modal">
|
||
<div class="gallery-head"><h2 class="ic-title"></h2><button class="wclose ic-close" type="button">✕</button></div>
|
||
<div class="ic-tools">
|
||
<input class="ic-search" type="search" autocomplete="off">
|
||
<select class="ic-theme-filter"></select>
|
||
<select class="ic-scope-filter"></select>
|
||
<select class="ic-folder-filter"></select>
|
||
<button class="tbtn ghost ic-folders" type="button"></button>
|
||
</div>
|
||
<div class="ic-quota"></div>
|
||
<details class="ic-add">
|
||
<summary></summary>
|
||
<div class="ic-add-grid">
|
||
<label class="ic-field"><span class="ic-upload-folder-label"></span><select class="ic-upload-folder"></select></label>
|
||
<label class="ic-field"><span class="ic-upload-themes-label"></span><select class="ic-upload-themes" multiple></select></label>
|
||
<label class="ed-upload ic-upload"><span></span><input type="file" accept="image/png,image/jpeg,image/gif,image/webp,image/avif" multiple></label>
|
||
</div>
|
||
<div class="ic-theme-new"><input type="text" maxlength="80"><button class="tbtn ghost" type="button"></button></div>
|
||
</details>
|
||
<div class="ic-status" role="status"></div>
|
||
<div class="ic-grid"></div>
|
||
<section class="ic-editor" hidden>
|
||
<h3></h3>
|
||
<div class="ic-editor-grid">
|
||
<label class="ic-field"><span class="ic-name-label"></span><input class="ic-edit-name" maxlength="80"></label>
|
||
<label class="ic-field"><span class="ic-folder-label"></span><input class="ic-edit-folder" maxlength="200"></label>
|
||
<label class="ic-field"><span class="ic-themes-label"></span><select class="ic-edit-themes" multiple></select></label>
|
||
</div>
|
||
<div class="ic-editor-actions"><button class="tbtn ic-save" type="button"></button><button class="tbtn danger ic-delete" type="button"></button><button class="tbtn ghost ic-cancel" type="button"></button></div>
|
||
</section>
|
||
</div>`;
|
||
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;
|
||
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=>!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 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);
|
||
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});
|
||
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. */
|
||
function renderImageStoragePanel(panel){
|
||
panel.innerHTML="";
|
||
const heading=document.createElement("div");heading.className="am-h3";heading.textContent=T("imgStorageAdmin");
|
||
const status=document.createElement("div");status.className="am-msg";panel.append(heading,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);
|
||
const editor=(item,type)=>{
|
||
const row=document.createElement("div");row.className="am-row img-quota-row";
|
||
const info=document.createElement("div");info.className="am-name";info.textContent=item.name||item.displayName;
|
||
const use=document.createElement("span");use.className="am-role";use.textContent=T("imgQuotaUsage").replace("{used}",imageBytes(item.usedBytes)).replace("{limit}",fmt(item.limitBytes));
|
||
info.append(document.createElement("br"),use);
|
||
const mode=document.createElement("select");mode.className="am-sel";
|
||
[["default","imgQuotaDefault"],["custom","imgQuotaCustom"],["unlimited","imgUnlimited"]].forEach(([v,k])=>mode.add(new Option(T(k),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}
|
||
};
|
||
row.append(info,mode,amount,save);return row;
|
||
};
|
||
(async()=>{
|
||
try{
|
||
const data=await api("/images/admin/quotas");host.innerHTML="";
|
||
const schoolTitle=document.createElement("div");schoolTitle.className="am-group";schoolTitle.textContent=T("imgSchools");
|
||
host.append(schoolTitle,...data.schools.map(s=>editor({...s,name:s.name},"schools")));
|
||
const userTitle=document.createElement("div");userTitle.className="am-group";userTitle.textContent=T("imgUsers");
|
||
const filter=document.createElement("input");filter.className="am-inp";filter.type="search";filter.placeholder=T("imgSearchUsers");
|
||
const users=document.createElement("div");users.className="img-quota-users";
|
||
const draw=()=>{const term=filter.value.trim().toLowerCase();users.innerHTML="";data.users.filter(u=>(u.displayName+" "+u.username).toLowerCase().includes(term)).forEach(u=>users.append(editor(u,"users")))};
|
||
filter.oninput=draw;host.append(userTitle,filter,users);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();
|