diff options
| author | Leonard Kugis <leonard@kug.is> | 2026-09-15 17:10:41 +0200 |
|---|---|---|
| committer | Leonard Kugis <leonard@kug.is> | 2026-09-15 17:10:41 +0200 |
| commit | 422d2e024cc5927280bfc9e2794797d510005728 (patch) | |
| tree | 0c95ebad21b30559afbc4076c1cd0bc99ac7e25b | |
| parent | ee6bd2b184cb79aca8445bccb458ce42edad9efa (diff) | |
| download | medman-422d2e024cc5927280bfc9e2794797d510005728.tar.gz | |
Implemented infinite scroll
| -rw-r--r-- | templates/index.html | 329 |
1 files changed, 241 insertions, 88 deletions
diff --git a/templates/index.html b/templates/index.html index da9c1ff..dfb4c2c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -105,10 +105,11 @@ main { flex:1; display:flex; overflow:hidden; } .card .chk { position:absolute; top:4px; left:4px; width:18px; height:18px; border-radius:4px; border:2px solid rgba(255,255,255,0.4); background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; font-size:0.6rem; color:#fff; cursor:pointer; z-index:5; } .card.sel .chk { background:var(--accent); border-color:var(--accent); } -#pg { display:flex; justify-content:center; align-items:center; gap:6px; margin-top:12px; } -#pg button { background:var(--surface); color:var(--text); border:1px solid var(--border); padding:3px 10px; border-radius:4px; cursor:pointer; font-size:0.7rem; } -#pg button:hover { border-color:var(--accent); } -#pg span { font-size:0.7rem; color:var(--text2); } +/* ── removed pagination; loading indicator ──────────────── */ +#load-more { display:flex; justify-content:center; padding:16px; } +#load-more .spinner { width:24px; height:24px; border:3px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin 0.8s linear infinite; display:none; } +#load-more .spinner.on { display:block; } +@keyframes spin { to { transform:rotate(360deg); } } .status { position:fixed; bottom:16px; right:16px; background:var(--surface2); border:1px solid var(--border); padding:8px 14px; border-radius:6px; font-size:0.75rem; z-index:300; opacity:0; transition:opacity 0.2s; pointer-events:none; } .status.on { opacity:1; } @@ -225,7 +226,7 @@ main { flex:1; display:flex; overflow:hidden; } <section id="content"> <div id="grid"></div> <div id="empty" style="color:var(--text2);text-align:center;padding:40px;display:none;">Keine Dateien gefunden.</div> - <div id="pg"></div> + <div id="load-more"><div class="spinner" id="load-spin"></div></div> </section> </main> @@ -234,13 +235,61 @@ main { flex:1; display:flex; overflow:hidden; } <div class="modal" id="conf-modal"><div class="mc" style="min-width:260px;text-align:center;"><p id="conf-msg"></p><button id="conf-yes" style="background:var(--red);color:#fff;border:none;padding:6px 16px;border-radius:4px;margin:6px;cursor:pointer;">Ja</button><button id="conf-no" style="background:var(--border);color:var(--text);border:none;padding:6px 16px;border-radius:4px;margin:6px;cursor:pointer;">Nein</button></div></div> <script> -const S = {files:[],disp:[],page:0,ps:50,sel:new Set(),tags:[],stags:[],sm:false,ph:null,_navKd:null}; +// ── State ────────────────────────────────────────────────────────────────── +const S = { + files:[], // all loaded file objects + sel:new Set(), // selected hashes + tags:[], // all known tags + stags:[], // search tag names (sidebar selection) + sm:false, // search mode active? + ph:null, // currently previewed hash + _navKd:null, // keyboard listener cleanup + _loading:false, // currently fetching more? + _exhausted:false, // all files loaded? + PAGE:80, // batch size + _obs:null, // IntersectionObserver instance + _sentinel:null, // sentinel element + _scrollTop:0, // saved scroll position when closing modal +}; function st(msg){let e=document.getElementById('status');e.textContent=msg;e.classList.add('on');clearTimeout(e._t);e._t=setTimeout(()=>e.classList.remove('on'),2200);} async function api(u,o={}){let r=await fetch(u,o);if(!r.ok){let e=await r.json().catch(()=>({error:r.statusText}));throw new Error(e.error||r.statusText);}return r.json();} -// ── Header & sidebar toggle ────────────────────────────────────────────── +// ── URL helpers ───────────────────────────────────────────────────────────── +function _updateURL(){ + let p=new URLSearchParams(); + if(S.sm && S.stags.length) p.set('tags',S.stags.join(',')); + else if(!S.sm) p.set('all','1'); + // encode scroll anchor: hash of last rendered file + if(S._rendered && S._rendered.length){ + let last=S._rendered[S._rendered.length-1]; + if(last) p.set('after',last.hash); + } + if(S.sel.size) p.set('sel',[...S.sel].join(',')); + let q=p.toString(); + let url=location.pathname+(q?'?'+q:''); + history.replaceState(null,'',url); +} + +function _loadStateFromURL(){ + let p=new URLSearchParams(location.search); + let tags=p.get('tags'); + if(tags){ + S.stags=tags.split(',').filter(Boolean); + S.sm=true; + } else if(p.has('all')){ + S.sm=false; S.stags=[]; + } else { + S.sm=false; S.stags=[]; + } + let sel=p.get('sel'); + if(sel) S._urlSel=new Set(sel.split(',').filter(Boolean)); + else S._urlSel=new Set(); + // 'after' is used to skip past already-loaded content on initial load + S._urlAfter=p.get('after')||null; +} +// ── Header & sidebar toggle ───────────────────────────────────────────────── function _persist(k,v){try{localStorage.setItem('medman_'+k,JSON.stringify(v));}catch(e){}} function _restore(k,def){try{let v=localStorage.getItem('medman_'+k);return v!==null?JSON.parse(v):def;}catch(e){return def;}} @@ -265,8 +314,7 @@ document.getElementById('b-toggle-side').addEventListener('click',()=>{ _persist('sidebar_visible',!collapsed); }); -// ── Section toggling ───────────────────────────────────────────────────── - +// ── Section toggling ──────────────────────────────────────────────────────── document.querySelectorAll('.sec-h').forEach(h=>{ h.addEventListener('click',()=>{ let sec=h.dataset.sec, b=document.getElementById('secb-'+sec); @@ -274,8 +322,7 @@ document.querySelectorAll('.sec-h').forEach(h=>{ }); }); -// ── Tags ───────────────────────────────────────────────────────────────── - +// ── Tags ──────────────────────────────────────────────────────────────────── async function lt(){S.tags=await api('/api/tags');rTagList();rSearchSuggestions('');} function rTagList(){ let l=document.getElementById('tag-list'); @@ -288,7 +335,7 @@ function rTagList(){ toggleSearchTag(c.dataset.tag); });}); l.querySelectorAll('.rm').forEach(b=>{b.addEventListener('click',async e=>{ - e.stopPropagation(); await api(`/api/tags/${b.dataset.tid}`,{method:'DELETE'}); await lt(); if(S.files.length) await lf(); st('Tag gelöscht'); + e.stopPropagation(); await api(`/api/tags/${b.dataset.tid}`,{method:'DELETE'}); await lt(); if(S.files.length) await reloadFiles(); st('Tag gelöscht'); });}); } @@ -298,8 +345,7 @@ function toggleSearchTag(name){ rTagList(); rSearchTags(); } -// ── Search ─────────────────────────────────────────────────────────────── - +// ── Search ────────────────────────────────────────────────────────────────── function rSearchTags(){ document.getElementById('search-tags').innerHTML=S.stags.map(n=> `<span class="tc sel">${n}<span class="rm" data-rm="${n}">×</span></span>` @@ -325,13 +371,15 @@ document.getElementById('si-in').addEventListener('input',function(){rSearchSugg document.getElementById('b-search').addEventListener('click',()=>{ if(!S.stags.length){st('Keine Tags ausgewählt');return;} - S.sm=true; let t=S.stags.join(','); - api(`/api/search?tags=${encodeURIComponent(t)}`).then(d=>{S.files=d;S.page=0;rGrid();}).catch(e=>{st('Suche fehlgeschlagen');S.files=[];rGrid();}); + S.sm=true; + resetAndReload(); +}); +document.getElementById('b-clear').addEventListener('click',()=>{ + S.sm=false; S.stags=[]; rTagList(); rSearchTags(); + resetAndReload(); }); -document.getElementById('b-clear').addEventListener('click',()=>{S.sm=false;S.stags=[];S.page=0;rTagList();rSearchTags();lf();}); - -// ── Tag create ─────────────────────────────────────────────────────────── +// ── Tag create ────────────────────────────────────────────────────────────── document.getElementById('ti-btn').addEventListener('click',async()=>{ let i=document.getElementById('ti-in'),n=i.value.trim(); if(!n)return; await api('/api/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:n})}); @@ -339,49 +387,137 @@ document.getElementById('ti-btn').addEventListener('click',async()=>{ }); document.getElementById('ti-in').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('ti-btn').click();}); -// ── Files ──────────────────────────────────────────────────────────────── - -async function lf(tf=null){let u='/api/files'+(tf?`?tag=${encodeURIComponent(tf)}`:'');S.files=await api(u);S.page=0;rGrid();} - -function rGrid(){ - let s=S.page*S.ps; S.disp=S.files.slice(s,s+S.ps); - let g=document.getElementById('grid'),e=document.getElementById('empty'); - if(!S.files.length){g.innerHTML='';e.style.display='block';document.getElementById('pg').innerHTML='';updateSelUI();return;} - e.style.display='none'; - g.innerHTML=S.disp.map(f=>{ - let sel=S.sel.has(f.hash)?'sel':'',mt=f.mt||'other', - tg=(f.tags||[]).map(t=>`<span class="mt">${t.name}</span>`).join(''), - sz=fs(f.size), ico={video:'🎬',image:'🖼️',audio:'🎵',doc:'📄',other:'📁'}[mt]||'📁', - ts=mt==='audio'?ico:`<img src="/thumb/${f.hash}" loading="lazy" alt="">`; - return `<div class="card ${sel}" data-hash="${f.hash}"> - <div class="chk">✓</div> - <div class="th">${ts}</div><div class="badge">${mt}</div> - <div class="info"><div class="hash" title="${f.hash}">${f.hash}</div><div class="meta">${sz} · DD${f.didx}</div></div> - <div class="ct">${tg}</div></div>`; - }).join(''); - g.querySelectorAll('.card').forEach(c=>{ - c.addEventListener('click',e=>{ - let h=c.dataset.hash; - if(e.ctrlKey||e.metaKey){toggleSel(h,c);return;} - if(e.target.classList.contains('chk')||e.target.closest('.chk')){ - toggleSel(h,c); e.stopPropagation(); return; - } - op(h); - }); +// ── Files: continuous scroll ──────────────────────────────────────────────── +// Fetch one batch of files. Returns empty array when exhausted. +async function _fetchBatch(afterHash=null){ + let params=new URLSearchParams(); + if(S.sm && S.stags.length){ + params.set('tags',S.stags.join(',')); + } + if(afterHash) params.set('after',afterHash); + params.set('limit',String(S.PAGE)); + + let endpoint=S.sm && S.stags.length?'/api/search':'/api/files'; + let url=endpoint+'?'+params.toString(); + return await api(url); +} + +function _cardHTML(f){ + let sel=S.sel.has(f.hash)?'sel':'',mt=f.mt||'other', + tg=(f.tags||[]).map(t=>`<span class="mt">${t.name}</span>`).join(''), + sz=fs(f.size), ico={video:'🎬',image:'🖼️',audio:'🎵',doc:'📄',other:'📁'}[mt]||'📁', + ts=mt==='audio'?ico:`<img src="/thumb/${f.hash}" loading="lazy" alt="">`; + return `<div class="card ${sel}" data-hash="${f.hash}"> + <div class="chk">✓</div> + <div class="th">${ts}</div><div class="badge">${mt}</div> + <div class="info"><div class="hash" title="${f.hash}">${f.hash}</div><div class="meta">${sz} · DD${f.didx}</div></div> + <div class="ct">${tg}</div></div>`; +} + +function _bindCardEvents(c){ + c.addEventListener('click',e=>{ + let h=c.dataset.hash; + if(e.ctrlKey||e.metaKey){toggleSel(h,c);return;} + if(e.target.classList.contains('chk')||e.target.closest('.chk')){ + toggleSel(h,c); e.stopPropagation(); return; + } + // Save scroll position before opening + S._scrollTop=document.getElementById('content').scrollTop; + op(h); }); - let tp=Math.ceil(S.files.length/S.ps); - document.getElementById('pg').innerHTML=tp<=1?'':`<button onclick="S.page=Math.max(0,S.page-1);rGrid();" ${S.page===0?'disabled':''}>←</button><span>${S.page+1}/${tp}</span><button onclick="S.page=Math.min(${tp-1},S.page+1);rGrid();" ${S.page>=tp-1?'disabled':''}>→</button>`; +} + +async function _loadMore(){ + if(S._loading||S._exhausted) return; + S._loading=true; + document.getElementById('load-spin').classList.add('on'); + try{ + let after=S._rendered&&S._rendered.length?S._rendered[S._rendered.length-1].hash:null; + let batch=await _fetchBatch(after); + if(!batch.length){S._exhausted=true; return;} + S.files.push(...batch); + _appendCards(batch); + S._rendered=Array.isArray(S._rendered)?S._rendered.concat(batch):batch.slice(); + }catch(e){ + st('Laden fehlgeschlagen'); + }finally{ + S._loading=false; + document.getElementById('load-spin').classList.remove('on'); + _updateURL(); + } +} + +function _appendCards(items){ + let g=document.getElementById('grid'); + for(let f of items){ + let t=document.createElement('div'); + t.innerHTML=_cardHTML(f); + let card=t.firstElementChild; + _bindCardEvents(card); + g.appendChild(card); + } + // Update sentinel + if(S._sentinel) S._sentinel.remove(); + let sentinel=document.createElement('div'); + sentinel.style.height='1px'; sentinel.id='obs-sentinel'; + document.getElementById('content').appendChild(sentinel); + S._sentinel=sentinel; + if(S._obs) S._obs.observe(sentinel); +} + +function _observeSentinel(){ + if(S._obs) S._obs.disconnect(); + S._obs=new IntersectionObserver(entries=>{ + if(entries[0].isIntersecting&&!S._loading&&!S._exhausted){ + _loadMore(); + } + },{root:document.getElementById('content'),rootMargin:'400px'}); +} + +async function resetAndReload(){ + // Clear grid + document.getElementById('grid').innerHTML=''; + document.getElementById('empty').style.display='none'; + if(S._sentinel){S._sentinel.remove();S._sentinel=null;} + S.files=[]; S._rendered=[]; S._exhausted=false; S._loading=false; + document.getElementById('load-spin').classList.remove('on'); + _updateURL(); + // Restore URL selections + S.sel.clear(); if(S._urlSel){for(let h of S._urlSel)S.sel.add(h);} updateSelUI(); + // Load initial batch + _observeSentinel(); + await _loadMore(); + // If grid still empty after first batch, show empty message + if(!S.files.length){ + document.getElementById('empty').style.display='block'; + } } -function fs(b){if(!b)return'';if(b<1024)return b+' B';if(b<1048576)return (b/1024).toFixed(1)+' KB';if(b<1073741824)return (b/1048576).toFixed(1)+' MB';return (b/1073741824).toFixed(1)+' GB';} +async function reloadFiles(){ + // Used after mutations (tag, delete, move): full reload preserving scroll + let scrollTop=document.getElementById('content').scrollTop; + document.getElementById('grid').innerHTML=''; + if(S._sentinel){S._sentinel.remove();S._sentinel=null;} + S.files=[]; S._rendered=[]; S._exhausted=false; S._loading=false; + document.getElementById('load-spin').classList.remove('on'); + _updateURL(); + _observeSentinel(); + await _loadMore(); + if(!S.files.length){ + document.getElementById('empty').style.display='block'; + } + // Restore scroll position (approximate) + requestAnimationFrame(()=>{document.getElementById('content').scrollTop=scrollTop;}); +} -// ── Selection ──────────────────────────────────────────────────────────── +function fs(b){if(!b)return'';if(b<1024)return b+' B';if(b<1048576)return (b/1024).toFixed(1)+' KB';if(b<1073741824)return (b/1048576).toFixed(1)+' MB';return (b/1073741824).toFixed(1)+' GB';} +// ── Selection ─────────────────────────────────────────────────────────────── function toggleSel(h,cardEl){ if(S.sel.has(h)){S.sel.delete(h);if(cardEl)cardEl.classList.remove('sel');} else{S.sel.add(h);if(cardEl)cardEl.classList.add('sel');} - updateSelUI(); + updateSelUI(); _updateURL(); } function updateSelUI(){ let n=S.sel.size; @@ -389,24 +525,32 @@ function updateSelUI(){ document.getElementById('sel-bar').classList.toggle('on',n>0); document.getElementById('sel-info').textContent=n?n+' Dateien ausgewählt':'Keine Auswahl'; } -function clearSel(){S.sel.clear();updateSelUI();rGrid();} -function selAll(){for(let f of S.disp){S.sel.add(f.hash);}updateSelUI();rGrid();} - -// ── Navigation helpers ─────────────────────────────────────────────────── +function clearSel(){S.sel.clear();updateSelUI();_updateURL(); refreshCardSelections();} +function selAll(){ + // Select all currently rendered cards + let cards=document.querySelectorAll('#grid .card'); + for(let c of cards){S.sel.add(c.dataset.hash);} + updateSelUI();_updateURL(); refreshCardSelections(); +} +function refreshCardSelections(){ + document.querySelectorAll('#grid .card').forEach(c=>{ + c.classList.toggle('sel',S.sel.has(c.dataset.hash)); + }); +} +// ── Navigation helpers ────────────────────────────────────────────────────── function _clearNavKd(){ if(S._navKd){document.removeEventListener('keydown',S._navKd);S._navKd=null;} } function opNav(delta){ if(!S.ph)return; let idx=S.files.findIndex(x=>x.hash===S.ph); if(idx<0)return; let nidx=idx+delta; if(nidx<0||nidx>=S.files.length)return; - let page=Math.floor(nidx/S.ps); - if(page!==S.page){S.page=page;rGrid();} + // Ensure next item is loaded if near the end + if(nidx>=S.files.length-S.PAGE/2 && !S._exhausted){_loadMore();} op(S.files[nidx].hash); } -// ── Selection actions ──────────────────────────────────────────────────── - +// ── Selection actions ─────────────────────────────────────────────────────── async function selTag(){ if(!S.sel.size){st('Keine Dateien ausgewählt');return;} let opts=S.tags.map(t=>`<option value="${t.id}">${t.name}</option>`).join(''); @@ -419,7 +563,7 @@ async function selTag(){ let s=document.getElementById('stag-sel'); if(!s.value)return; let tid=parseInt(s.value),c=0; for(let h of[...S.sel]){try{await api(`/api/files/${h}/tags`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tid})});c++;}catch(e){}} - document.getElementById('prev-modal').classList.remove('on'); st(`${c} getaggt`); await lf(S.sm?S.stags[0]:null); + document.getElementById('prev-modal').classList.remove('on'); st(`${c} getaggt`); await reloadFiles(); }; } @@ -440,7 +584,7 @@ async function selUntag(){ let s=document.getElementById('suntag-sel'); if(!s.value)return; let tid=parseInt(s.value),c=0; for(let h of[...S.sel]){try{await api(`/api/files/${h}/tags/${tid}`,{method:'DELETE'});c++;}catch(e){}} - document.getElementById('prev-modal').classList.remove('on'); st(`${c} Tags entfernt`); await lf(S.sm?S.stags[0]:null); + document.getElementById('prev-modal').classList.remove('on'); st(`${c} Tags entfernt`); await reloadFiles(); }; } @@ -456,7 +600,7 @@ async function selMove(){ let s=document.getElementById('smv-sel'); if(!s.value)return; let ti=parseInt(s.value),c=0; for(let h of[...S.sel]){try{await api(`/api/files/${h}/move`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({didx:ti})});c++;}catch(e){}} - document.getElementById('prev-modal').classList.remove('on'); clearSel(); st(`${c} verschoben`); await lf(S.sm?S.stags[0]:null); + document.getElementById('prev-modal').classList.remove('on'); clearSel(); st(`${c} verschoben`); await reloadFiles(); }; } @@ -464,16 +608,16 @@ async function selDel(){ if(!S.sel.size){st('Keine Dateien ausgewählt');return;} sc(`${S.sel.size} Dateien wirklich löschen?`,async()=>{ let c=0; for(let h of[...S.sel]){try{await api(`/api/files/${h}`,{method:'DELETE'});c++;}catch(e){}} - clearSel(); st(`${c} gelöscht`); await lf(S.sm?S.stags[0]:null); + clearSel(); st(`${c} gelöscht`); await reloadFiles(); }); } function sc(msg,cb){document.getElementById('conf-msg').textContent=msg;document.getElementById('conf-modal').classList.add('on');document.getElementById('conf-yes').onclick=()=>{document.getElementById('conf-modal').classList.remove('on');cb();};} -// ── Single file preview ────────────────────────────────────────────────── - +// ── Single file preview ───────────────────────────────────────────────────── async function op(h){ _clearNavKd(); + S._scrollTop=document.getElementById('content').scrollTop; S.ph=h; let f=S.files.find(x=>x.hash===h); if(!f)return; let ft=await api(`/api/files/${h}/tags`),dd=await api('/api/dat'), ht=`<h2>${h}</h2>`,mt=f.mt||'other'; @@ -520,23 +664,21 @@ async function op(h){ let ab=document.getElementById('atb'); if(ab)ab.onclick=async()=>{ let s=document.getElementById('ats'); if(!s.value)return; await api(`/api/files/${h}/tags`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tid:parseInt(s.value)})}); - st('Tag hinzugefügt'); await lf(S.sm?S.stags[0]:null); op(h);}; + st('Tag hinzugefügt'); await reloadFiles(); op(h);}; let mb=document.getElementById('mvb'); if(mb)mb.onclick=async()=>{ let s=document.getElementById('mds'); if(!s.value)return; await api(`/api/files/${h}/move`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({didx:parseInt(s.value)})}); - st('Verschoben'); await lf(S.sm?S.stags[0]:null); op(h);}; + st('Verschoben'); await reloadFiles(); op(h);}; let db=document.getElementById('delb'); if(db)db.onclick=()=>{ - sc(`Diese Datei wirklich löschen?`,async()=>{await api(`/api/files/${h}`,{method:'DELETE'}); st('Gelöscht'); S.sel.delete(h); document.getElementById('prev-modal').classList.remove('on'); await lf(S.sm?S.stags[0]:null);});}; + sc(`Diese Datei wirklich löschen?`,async()=>{await api(`/api/files/${h}`,{method:'DELETE'}); st('Gelöscht'); S.sel.delete(h); document.getElementById('prev-modal').classList.remove('on'); await reloadFiles();});}; document.querySelectorAll('#m-body .rm').forEach(b=>{b.onclick=async e=>{e.stopPropagation(); - await api(`/api/files/${h}/tags/${b.dataset.tid}`,{method:'DELETE'}); st('Tag entfernt'); await lf(S.sm?S.stags[0]:null); op(h);};}); + await api(`/api/files/${h}/tags/${b.dataset.tid}`,{method:'DELETE'}); st('Tag entfernt'); await reloadFiles(); op(h);};}); } -// ── Data dirs ──────────────────────────────────────────────────────────── - +// ── Data dirs ─────────────────────────────────────────────────────────────── async function ldd(){let d=await api('/api/dat');document.getElementById('dd-info').innerHTML=d.dat.map(x=>`<div class="dd" style="margin-bottom:4px;"><strong>DD${x.i}</strong> (${(x.w*100).toFixed(0)}%)<br>${x.n} Dateien, ${(x.bytes/(1024*1024*1024)).toFixed(1)} GB<br><span style="opacity:0.5;word-break:break-all;font-size:0.55rem;">${x.path}</span></div>`).join('')+(d.src.length?`<div style="margin-top:6px;"><strong>Quellen:</strong><br>${d.src.map(s=>'<span style="opacity:0.5;font-size:0.55rem;">'+s+'</span>').join('<br>')}</div>`:'');} -// ── Header buttons ─────────────────────────────────────────────────────── - +// ── Header buttons ────────────────────────────────────────────────────────── document.getElementById('b-up').addEventListener('click',()=>document.getElementById('up-area').classList.toggle('on')); document.getElementById('up-cancel').addEventListener('click',()=>document.getElementById('up-area').classList.remove('on')); document.getElementById('up-go').addEventListener('click',async()=>{ @@ -544,28 +686,26 @@ document.getElementById('up-go').addEventListener('click',async()=>{ let fd=new FormData(); for(let f of i.files)fd.append('file',f); p.textContent=`Lade ${i.files.length} Datei(en)...`; try{let r=await fetch('/api/upload',{method:'POST',body:fd}),d=await r.json(),ok=d.results.filter(x=>x.status==='ok').length; - p.textContent=`${ok} von ${d.results.length} hochgeladen.`; i.value=''; await lf(); await ldd(); + p.textContent=`${ok} von ${d.results.length} hochgeladen.`; i.value=''; await reloadFiles(); await ldd(); setTimeout(()=>p.textContent='',3000);}catch(e){p.textContent='Fehler: '+e.message;} }); -document.getElementById('b-scan').addEventListener('click',async()=>{st('Scanne...');let r=await api('/api/scan',{method:'POST'});st(`Scan: ${r.ingested} neu`);await lf();await ldd();}); +document.getElementById('b-scan').addEventListener('click',async()=>{st('Scanne...');let r=await api('/api/scan',{method:'POST'});st(`Scan: ${r.ingested} neu`);await reloadFiles();await ldd();}); document.getElementById('b-exp').addEventListener('click',async()=>{ let d=await api('/api/tags/export'),bl=new Blob([JSON.stringify(d,null,2)],{type:'application/json'}),u=URL.createObjectURL(bl),a=document.createElement('a'); a.href=u;a.download='medman_tags_'+new Date().toISOString().slice(0,10)+'.json';a.click();URL.revokeObjectURL(u);st('Exportiert'); }); document.getElementById('f-imp').addEventListener('change',async e=>{ let f=e.target.files[0];if(!f)return;let t=await f.text(),d=JSON.parse(t),r=await api('/api/tags/import',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)}); - await lt();if(S.files.length)await lf();st(`${r.imported||0} importiert`); + await lt();if(S.files.length)await reloadFiles();st(`${r.imported||0} importiert`); }); -// ── Selection bar ──────────────────────────────────────────────────────── - +// ── Selection bar ─────────────────────────────────────────────────────────── document.getElementById('sel-tag-btn').addEventListener('click',selTag); document.getElementById('sel-mv-btn').addEventListener('click',selMove); document.getElementById('sel-del-btn').addEventListener('click',selDel); document.getElementById('sel-clr-btn').addEventListener('click',clearSel); -// ── Selection section ──────────────────────────────────────────────────── - +// ── Selection section ─────────────────────────────────────────────────────── document.getElementById('b-sel-all').addEventListener('click',selAll); document.getElementById('b-sel-tag').addEventListener('click',selTag); document.getElementById('b-sel-untag').addEventListener('click',selUntag); @@ -573,22 +713,35 @@ document.getElementById('b-sel-mv').addEventListener('click',selMove); document.getElementById('b-sel-del').addEventListener('click',selDel); document.getElementById('b-sel-clr').addEventListener('click',clearSel); -// ── Modals ─────────────────────────────────────────────────────────────── - +// ── Modals ────────────────────────────────────────────────────────────────── function closeModal(){ _clearNavKd(); document.getElementById('prev-modal').classList.remove('on'); S.ph=null; - lf(S.sm?S.stags[0]:null); + // Restore scroll position + requestAnimationFrame(()=>{ + document.getElementById('content').scrollTop=S._scrollTop||0; + }); } document.getElementById('m-close').addEventListener('click',closeModal); document.getElementById('prev-modal').addEventListener('click',e=>{if(e.target===e.currentTarget)closeModal();}); document.getElementById('conf-no').addEventListener('click',()=>document.getElementById('conf-modal').classList.remove('on')); document.addEventListener('keydown',e=>{if(e.key==='Escape'){if(document.getElementById('prev-modal').classList.contains('on'))closeModal();}}); -// ── Init ───────────────────────────────────────────────────────────────── - -lt(); lf(); ldd(); +// ── Init ──────────────────────────────────────────────────────────────────── +_loadStateFromURL(); +lt(); ldd(); +_observeSentinel(); +_loadMore().then(()=>{ + if(!S.files.length) document.getElementById('empty').style.display='block'; + // Apply URL selections + if(S._urlSel&&S._urlSel.size){ + for(let h of S._urlSel) S.sel.add(h); + updateSelUI(); + refreshCardSelections(); + } + _updateURL(); +}); </script> </body> </html> |
