diff options
Diffstat (limited to 'templates/index.html')
| -rw-r--r-- | templates/index.html | 534 |
1 files changed, 534 insertions, 0 deletions
diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9f48eb7 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,534 @@ +<!DOCTYPE html> +<html lang="de"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>MedMan</title> +<style> +:root { + --bg: #0f0f1a; --surface: #181825; --surface2: #1e1e2e; + --border: #313244; --text: #cdd6f4; --text2: #a6adc8; + --accent: #89b4fa; --red: #f38ba8; --green: #a6e3a1; + --yellow: #f9e2af; --purple: #cba6f7; +} +* { margin:0; padding:0; box-sizing:border-box; } +body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; display:flex; flex-direction:column; } +header { background:var(--surface); border-bottom:1px solid var(--border); padding:8px 16px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; position:sticky; top:0; z-index:100; } +header h1 { font-size:1.1rem; font-weight:700; white-space:nowrap; color:var(--accent); margin-right:auto; } +header button, header .btn { background:var(--surface2); color:var(--text); border:1px solid var(--border); border-radius:5px; padding:4px 10px; cursor:pointer; font-size:0.75rem; white-space:nowrap; } +header button:hover { background:var(--border); } +.btn-a { background:var(--accent)!important; color:#000!important; border-color:var(--accent)!important; font-weight:600; } +.btn-a:hover { filter:brightness(1.1); } +.btn-g { background:var(--green)!important; color:#000!important; border-color:var(--green)!important; font-weight:600; } +.btn-g:hover { filter:brightness(1.1); } +#sel-bar { display:none; align-items:center; gap:6px; padding:4px 16px; background:var(--accent); color:#000; font-size:0.75rem; font-weight:600; } +#sel-bar.on { display:flex; } +#sel-bar button { background:rgba(0,0,0,0.2); color:#000; border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-size:0.7rem; font-weight:600; } +#sel-bar button:hover { background:rgba(0,0,0,0.35); } + +#up-area { display:none; padding:8px 16px; background:var(--surface); border-bottom:1px solid var(--border); } +#up-area.on { display:flex; align-items:center; gap:8px; flex-wrap:wrap; } +#up-area input[type=file] { color:var(--text2); font-size:0.75rem; } +#up-area .prog { color:var(--accent); font-size:0.75rem; } + +main { flex:1; display:flex; overflow:hidden; } +#side { width:240px; background:var(--surface); border-right:1px solid var(--border); padding:10px; overflow-y:auto; flex-shrink:0; display:flex; flex-direction:column; gap:6px; } + +.section { border:1px solid var(--border); border-radius:7px; overflow:hidden; } +.sec-h { display:flex; align-items:center; justify-content:space-between; padding:7px 10px; background:var(--surface2); cursor:pointer; user-select:none; } +.sec-h:hover { background:var(--border); } +.sec-h .t { font-size:0.72rem; font-weight:600; text-transform:uppercase; letter-spacing:0.04em; color:var(--text2); } +.sec-h .arr { font-size:0.65rem; transition:transform 0.2s; color:var(--text2); } +.sec-h.open .arr { transform:rotate(90deg); } +.sec-b { display:none; padding:8px; } +.sec-b.on { display:block; } + +#tag-list { display:flex; flex-wrap:wrap; gap:3px; } +.tc { display:inline-flex; align-items:center; gap:3px; padding:2px 7px; border-radius:999px; font-size:0.72rem; cursor:pointer; transition:all 0.15s; border:1px solid var(--border); user-select:none; background:var(--surface2); color:var(--text); } +.tc:hover { border-color:var(--accent); } +.tc.sel { border-color:#fff; background:var(--accent); color:#000; } +.tc .rm { font-size:0.6rem; opacity:0.4; cursor:pointer; margin-left:1px; } +.tc .rm:hover { opacity:1; } +.ti-row { display:flex; gap:3px; margin-top:6px; } +.ti-row input { flex:1; background:var(--bg); border:1px solid var(--border); border-radius:4px; padding:4px 6px; color:var(--text); font-size:0.72rem; } +.ti-row button { background:var(--accent); color:#000; border:none; border-radius:4px; padding:4px 8px; cursor:pointer; font-weight:600; font-size:0.72rem; } + +#si-row { margin-bottom:6px; } +#si-row input { width:100%; background:var(--bg); border:1px solid var(--border); border-radius:4px; padding:4px 6px; color:var(--text); font-size:0.72rem; } +#suggestions { display:flex; flex-wrap:wrap; gap:3px; margin-bottom:4px; } +#suggestions .tc { cursor:pointer; opacity:0.7; } +#suggestions .tc:hover { opacity:1; border-color:var(--accent); } + +.srch-acts { display:flex; flex-direction:column; gap:3px; margin-top:4px; } +.srch-acts button { border:none; border-radius:4px; padding:3px 8px; cursor:pointer; font-weight:600; font-size:0.68rem; color:#000; } +.srch-acts .s { background:var(--green); } +.srch-acts .c { background:var(--red); } + +.sel-acts { display:flex; flex-direction:column; gap:3px; } +.sel-acts button { background:var(--surface2); color:var(--text2); border:1px solid var(--border); border-radius:4px; padding:3px 8px; cursor:pointer; font-size:0.68rem; text-align:left; } +.sel-acts button:hover { color:var(--text); border-color:var(--accent); } + +#dd-info { font-size:0.62rem; color:var(--text2); } +#dd-info .dd { margin:1px 0; } + +#content { flex:1; overflow-y:auto; padding:12px; } +#grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(170px,1fr)); gap:8px; } +.card { background:var(--surface); border:1px solid var(--border); border-radius:7px; overflow:hidden; transition:all 0.15s; cursor:pointer; position:relative; } +.card:hover { border-color:var(--accent); transform:translateY(-1px); } +.card.sel { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent); } +.card .th { width:100%; aspect-ratio:4/3; object-fit:cover; background:var(--surface2); display:flex; align-items:center; justify-content:center; font-size:1.6rem; } +.card .th img { width:100%; height:100%; object-fit:cover; } +.card .info { padding:6px 7px; } +.card .hash { font-size:0.65rem; font-weight:500; word-break:break-all; line-height:1.2; font-family:monospace; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; color:var(--accent); } +.card .meta { font-size:0.58rem; color:var(--text2); margin-top:1px; } +.card .badge { position:absolute; top:4px; right:4px; background:rgba(0,0,0,0.75); color:var(--text); padding:1px 5px; border-radius:3px; font-size:0.5rem; text-transform:uppercase; letter-spacing:0.03em; } +.card .ct { display:flex; flex-wrap:wrap; gap:2px; padding:0 7px 4px; } +.card .ct .mt { font-size:0.5rem; padding:1px 4px; border-radius:999px; background:var(--surface2); color:var(--text2); } +.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); } + +.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; } + +.modal { display:none; position:fixed; inset:0; z-index:200; background:rgba(0,0,0,0.82); align-items:center; justify-content:center; } +.modal.on { display:flex; } +.mc { background:var(--surface); border:1px solid var(--border); border-radius:10px; max-width:90vw; max-height:90vh; overflow-y:auto; padding:18px; min-width:340px; position:relative; } +.mc .cb { position:absolute; top:5px; right:9px; background:none; border:none; color:var(--text); font-size:1.2rem; cursor:pointer; } +.mc img, .mc video { max-width:100%; max-height:48vh; border-radius:6px; display:block; margin:0 auto; } +.mc h2 { margin-bottom:6px; font-size:0.8rem; word-break:break-all; font-family:monospace; color:var(--accent); } +.mc .ft { display:flex; flex-wrap:wrap; gap:3px; margin-top:6px; } +.mc .mrow { margin-top:5px; font-size:0.68rem; color:var(--text2); } +.mc .ar { margin-top:8px; display:flex; gap:4px; flex-wrap:wrap; } +.mc select, .mc input { background:var(--surface2); color:var(--text); border:1px solid var(--border); border-radius:3px; padding:3px 6px; font-size:0.72rem; } +.mc button.act { background:var(--accent); color:#000; border:none; border-radius:3px; padding:3px 8px; cursor:pointer; font-weight:600; font-size:0.7rem; } +.mc button.act:hover { filter:brightness(1.1); } +.mc button.dng { background:var(--red); } + +.mc .nav-row { display:flex; align-items:center; gap:6px; margin-top:12px; padding-top:8px; border-top:1px solid var(--border); } +.mc .nav-row button { flex:1; background:var(--surface2); color:var(--text); border:1px solid var(--border); border-radius:4px; padding:5px 10px; cursor:pointer; font-size:0.7rem; } +.mc .nav-row button:hover { border-color:var(--accent); } +.mc .nav-row button:disabled { opacity:0.3; cursor:default; } +.mc .nav-row span { font-size:0.62rem; color:var(--text2); white-space:nowrap; min-width:60px; text-align:center; } +</style> +</head> +<body> +<header> + <h1>🧩 MedMan</h1> + <button id="b-up" class="btn-a">📤 Upload</button> + <button id="b-scan" class="btn-g">🔄 Quellen scannen</button> + <button id="b-exp">📦 Export</button> + <label class="btn" style="cursor:pointer;">📥 Import <input type="file" id="f-imp" accept=".json" style="display:none;"></label> +</header> + +<div id="sel-bar"> + <span id="sel-n">0 ausgewählt</span> + <button id="sel-tag-btn">🏷 Tags</button> + <button id="sel-mv-btn">📁 Verschieben</button> + <button id="sel-del-btn" style="background:rgba(180,0,0,0.4)">🗑 Löschen</button> + <button id="sel-clr-btn">✕</button> +</div> + +<div id="up-area"> + <input type="file" id="up-in" multiple> + <button id="up-go" class="btn-a">Hochladen</button> + <span class="prog" id="up-prog"></span> + <button id="up-cancel">Abbrechen</button> +</div> + +<main> + <aside id="side"> + + <!-- Tags section --> + <div class="section" id="sec-tags"> + <div class="sec-h open" data-sec="tags"><span class="t">🏷 Tags</span><span class="arr">▶</span></div> + <div class="sec-b on" id="secb-tags"> + <div id="tag-list"></div> + <div class="ti-row"> + <input id="ti-in" type="text" placeholder="Neuer Tag..." maxlength="50"> + <button id="ti-btn">+</button> + </div> + </div> + </div> + + <!-- Search section --> + <div class="section" id="sec-search"> + <div class="sec-h" data-sec="search"><span class="t">🔍 Suche</span><span class="arr">▶</span></div> + <div class="sec-b" id="secb-search"> + <div id="si-row"> + <input id="si-in" type="text" placeholder="Tag-Suche (Substring)..." autocomplete="off"> + </div> + <div id="suggestions"></div> + <div style="font-size:0.62rem;color:var(--text2);margin:2px 0;">Ausgewählte Tags:</div> + <div id="search-tags"></div> + <div class="srch-acts"> + <button class="s" id="b-search">🔍 Dateien finden</button> + <button class="c" id="b-clear">✕ Suche löschen</button> + </div> + </div> + </div> + + <!-- Selection section --> + <div class="section" id="sec-sel"> + <div class="sec-h" data-sec="sel"><span class="t">☑ Auswahl</span><span class="arr">▶</span></div> + <div class="sec-b" id="secb-sel"> + <div style="font-size:0.65rem;color:var(--text2);margin-bottom:4px;" id="sel-info">Keine Auswahl</div> + <div class="sel-acts"> + <button id="b-sel-all">✅ Alle auswählen</button> + <button id="b-sel-tag">🏷 Tags zuweisen</button> + <button id="b-sel-untag">🚫 Tags entfernen</button> + <button id="b-sel-mv">📁 Verschieben</button> + <button id="b-sel-del" style="color:var(--red)">🗑 Löschen</button> + <button id="b-sel-clr">✕ Auswahl aufheben</button> + </div> + </div> + </div> + + <!-- Data dirs section --> + <div class="section"> + <div class="sec-h open" data-sec="dd"><span class="t">📊 Daten</span><span class="arr">▶</span></div> + <div class="sec-b on" id="secb-dd"> + <div id="dd-info">Lade...</div> + <div style="font-size:0.58rem;color:var(--text2);margin-top:6px;"> + <a href="/api/tags/export" target="_blank" style="color:var(--accent)">Roh-Export (JSON)</a> + </div> + </div> + </div> + </aside> + + <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> + </section> +</main> + +<div class="status" id="status"></div> +<div class="modal" id="prev-modal"><div class="mc"><button class="cb" id="m-close">×</button><div id="m-body"></div></div></div> +<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}; + +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();} + +// --- Section toggling --- +document.querySelectorAll('.sec-h').forEach(h=>{ + h.addEventListener('click',()=>{ + let sec=h.dataset.sec, b=document.getElementById('secb-'+sec); + h.classList.toggle('open'); b.classList.toggle('on'); + }); +}); + +// --- Tags --- +async function lt(){S.tags=await api('/api/tags');rTagList();rSearchSuggestions('');} +function rTagList(){ + let l=document.getElementById('tag-list'); + l.innerHTML=S.tags.map(t=>{ + let sel=S.stags.includes(t.name)?'sel':''; + return `<span class="tc ${sel}" data-tag="${t.name}">${t.name}<span class="rm" data-tid="${t.id}">×</span></span>`; + }).join(''); + l.querySelectorAll('.tc').forEach(c=>{c.addEventListener('click',e=>{ + if(e.target.classList.contains('rm'))return; + 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'); + });}); +} + +function toggleSearchTag(name){ + let i=S.stags.indexOf(name); + if(i>=0) S.stags.splice(i,1); else S.stags.push(name); + rTagList(); rSearchTags(); +} + +// --- 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>` + ).join(''); + document.querySelectorAll('#search-tags .rm').forEach(b=>{b.addEventListener('click',e=>{ + e.stopPropagation(); S.stags=S.stags.filter(x=>x!==b.dataset.rm); rTagList(); rSearchTags(); + });}); +} + +function rSearchSuggestions(q){ + let el=document.getElementById('suggestions'); + if(!q){el.innerHTML='';return;} + let re=new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'),'i'); + let matches=S.tags.filter(t=>re.test(t.name)&&!S.stags.includes(t.name)).slice(0,12); + el.innerHTML=matches.map(t=>`<span class="tc" data-tag="${t.name}">${t.name}</span>`).join(''); + el.querySelectorAll('.tc').forEach(c=>{c.addEventListener('click',()=>{ + S.stags.push(c.dataset.tag); rTagList(); rSearchTags(); rSearchSuggestions(''); + document.getElementById('si-in').value=''; + });}); +} + +document.getElementById('si-in').addEventListener('input',function(){rSearchSuggestions(this.value.trim());}); + +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();}); +}); +document.getElementById('b-clear').addEventListener('click',()=>{S.sm=false;S.stags=[];S.page=0;rTagList();rSearchTags();lf();}); + +// --- 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})}); + i.value=''; await lt(); st('Tag erstellt'); +}); +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 the click was on the checkbox itself: toggle selection, don't open + if(e.target.classList.contains('chk')||e.target.closest('.chk')){ + toggleSel(h,c); e.stopPropagation(); return; + } + 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>`; + updateSelUI(); +} + +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(); +} +function updateSelUI(){ + let n=S.sel.size; + document.getElementById('sel-n').textContent=n+' ausgewählt'; + 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(){let pageStart=S.page*S.ps;for(let f of S.disp){S.sel.add(f.hash);}updateSelUI();rGrid();} + +// --- 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();} + op(S.files[nidx].hash); +} + +// --- 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(''); + document.getElementById('m-body').innerHTML= + `<h3>${S.sel.size} Dateien: Tag zuweisen</h3> + <select id="stag-sel" style="width:100%;margin-top:6px;"><option value="">-- Tag wählen --</option>${opts}</select> + <div style="margin-top:6px;"><button class="act" id="stag-go">Zuweisen</button></div>`; + document.getElementById('prev-modal').classList.add('on'); + document.getElementById('stag-go').onclick=async()=>{ + 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); + }; +} + +async function selUntag(){ + if(!S.sel.size){st('Keine Dateien ausgewählt');return;} + let allTids=new Map(); + let promises=[...S.sel].map(h=>api(`/api/files/${h}/tags`)); + let results=await Promise.allSettled(promises); + for(let r of results){if(r.status==='fulfilled'){for(let t of r.value){allTids.set(t.id,t.name);}}} + if(!allTids.size){st('Keine Tags auf der Auswahl');return;} + let opts=[...allTids.entries()].map(([id,name])=>`<option value="${id}">${name}</option>`).join(''); + document.getElementById('m-body').innerHTML= + `<h3>${S.sel.size} Dateien: Tag entfernen</h3> + <select id="suntag-sel" style="width:100%;margin-top:6px;"><option value="">-- Tag wählen --</option>${opts}</select> + <div style="margin-top:6px;"><button class="act dng" id="suntag-go">Entfernen</button></div>`; + document.getElementById('prev-modal').classList.add('on'); + document.getElementById('suntag-go').onclick=async()=>{ + 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); + }; +} + +async function selMove(){ + if(!S.sel.size){st('Keine Dateien ausgewählt');return;} + let dd=await api('/api/dat'),opts=dd.dat.map(d=>`<option value="${d.i}">DD${d.i} (${d.path})</option>`).join(''); + document.getElementById('m-body').innerHTML= + `<h3>${S.sel.size} Dateien verschieben</h3> + <select id="smv-sel" style="width:100%;margin-top:6px;"><option value="">-- Ziel --</option>${opts}</select> + <div style="margin-top:6px;"><button class="act" id="smv-go">Verschieben</button></div>`; + document.getElementById('prev-modal').classList.add('on'); + document.getElementById('smv-go').onclick=async()=>{ + 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); + }; +} + +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); + }); +} + +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 --- +async function op(h){ + _clearNavKd(); + 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'; + if(mt==='image')ht+=`<img src="/raw/${h}" alt="">`; + else if(mt==='video')ht+=`<video controls src="/raw/${h}" style="max-width:100%;max-height:48vh;"></video>`; + else if(mt==='audio')ht+=`<audio controls src="/raw/${h}" style="width:100%;"></audio>`; + else ht+=`<p style="color:var(--text2)">Keine Vorschau</p>`; + ht+=`<div class="mrow">Typ: ${mt} · Größe: ${fs(f.size)} · DD${f.didx}<br>${f.w&&f.h?`${f.w}×${f.h} · `:''}${f.dur?parseFloat(f.dur).toFixed(1)+'s · ':''}${f.ts||''}</div>`; + ht+=`<div class="ft">`; + for(let t of ft)ht+=`<span class="tc" style="background:var(--surface2)">${t.name}<span class="rm" data-tid="${t.id}" data-hash="${h}">×</span></span>`; + ht+=`</div>`; + ht+=`<div class="ar"><select id="ats"><option value="">-- Tag hinzufügen --</option>`; + for(let t of S.tags)if(!ft.some(x=>x.id===t.id))ht+=`<option value="${t.id}">${t.name}</option>`; + ht+=`</select><button class="act" id="atb">+</button></div>`; + ht+=`<div class="ar"><select id="mds"><option value="">-- Verschieben nach --</option>`; + for(let d of dd.dat)if(d.i!==f.didx)ht+=`<option value="${d.i}">DD${d.i}</option>`; + ht+=`</select><button class="act" id="mvb">Verschieben</button></div>`; + ht+=`<div class="ar"><button class="act dng" id="delb">🗑 Löschen</button></div>`; + ht+=`<p style="margin-top:6px;font-size:0.6rem;"><a href="/raw/${h}" target="_blank" style="color:var(--accent)">Direktlink</a></p>`; + + // ── Prev / Next navigation row ──────────────────────────────────── + let idx=S.files.findIndex(x=>x.hash===h); + let hasPrev=idx>0, hasNext=idx>=0 && idx<S.files.length-1; + let prevLabel=hasPrev?`← ${S.files[idx-1].hash.slice(0,8)}…`:'← ──'; + let nextLabel=hasNext?`${S.files[idx+1].hash.slice(0,8)}… →`:'── →'; + ht+=`<div class="nav-row"> + <button id="nav-prev" ${hasPrev?'':'disabled'}>${prevLabel}</button> + <span>${idx+1} / ${S.files.length}</span> + <button id="nav-next" ${hasNext?'':'disabled'}>${nextLabel}</button> + </div>`; + + document.getElementById('m-body').innerHTML=ht; document.getElementById('prev-modal').classList.add('on'); + + // Prev / Next button handlers + let np=document.getElementById('nav-prev'), nn=document.getElementById('nav-next'); + if(np)np.onclick=e=>{e.stopPropagation();opNav(-1);}; + if(nn)nn.onclick=e=>{e.stopPropagation();opNav(1);}; + + // Keyboard left/right navigation while modal is open + S._navKd=(e)=>{ + if(e.key==='ArrowLeft'){e.preventDefault();opNav(-1);} + else if(e.key==='ArrowRight'){e.preventDefault();opNav(1);} + }; + document.addEventListener('keydown',S._navKd); + + 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);}; + 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);}; + 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);});}; + 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);};}); +} + +// --- 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 --- +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()=>{ + let i=document.getElementById('up-in'),p=document.getElementById('up-prog'); if(!i.files.length)return; + 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(); + 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-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`); +}); + +// --- 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 --- +document.getElementById('b-sel-all').addEventListener('click',selAll); +document.getElementById('b-sel-tag').addEventListener('click',selTag); +document.getElementById('b-sel-untag').addEventListener('click',selUntag); +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 --- +function closeModal(){ + _clearNavKd(); + document.getElementById('prev-modal').classList.remove('on'); + S.ph=null; + lf(S.sm?S.stags[0]:null); +} +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(); +</script> +</body> +</html> |
