aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonard Kugis <leonard@kug.is>2026-09-15 17:10:41 +0200
committerLeonard Kugis <leonard@kug.is>2026-09-15 17:10:41 +0200
commit422d2e024cc5927280bfc9e2794797d510005728 (patch)
tree0c95ebad21b30559afbc4076c1cd0bc99ac7e25b
parentee6bd2b184cb79aca8445bccb458ce42edad9efa (diff)
downloadmedman-422d2e024cc5927280bfc9e2794797d510005728.tar.gz
Implemented infinite scroll
-rw-r--r--templates/index.html329
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}">&times;</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>