knxdisplay/main/embedded/filemanager.html
2026-01-23 16:46:09 +01:00

223 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SD Card File Manager</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#1a1a2e;color:#eee;min-height:100vh;padding:1rem}
.container{max-width:900px;margin:0 auto}
h1{color:#00d4ff;margin-bottom:1rem;font-size:1.5rem}
.status{background:#16213e;padding:0.75rem;border-radius:8px;margin-bottom:1rem;display:flex;gap:1rem;flex-wrap:wrap;align-items:center}
.status-item{display:flex;align-items:center;gap:0.5rem}
.status-dot{width:10px;height:10px;border-radius:50%}
.status-dot.ok{background:#0f0}
.status-dot.err{background:#f00}
.toolbar{display:flex;gap:0.5rem;margin-bottom:1rem;flex-wrap:wrap}
.btn{background:#0f3460;color:#fff;border:none;padding:0.5rem 1rem;border-radius:6px;cursor:pointer;font-size:0.9rem;transition:background 0.2s}
.btn:hover{background:#1a4f8c}
.btn:disabled{opacity:0.5;cursor:not-allowed}
.btn-danger{background:#c0392b}
.btn-danger:hover{background:#e74c3c}
.btn-small{padding:0.3rem 0.6rem;font-size:0.8rem}
.path-bar{background:#16213e;padding:0.75rem;border-radius:8px;margin-bottom:1rem;display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap}
.path-segment{color:#00d4ff;cursor:pointer;text-decoration:underline}
.path-segment:hover{color:#fff}
.file-list{background:#16213e;border-radius:8px;overflow:hidden}
.file-item{display:flex;align-items:center;padding:0.75rem 1rem;border-bottom:1px solid #0f3460;cursor:pointer;transition:background 0.2s}
.file-item:hover{background:#1a3a5c}
.file-item.selected{background:#0f3460}
.file-item:last-child{border-bottom:none}
.file-icon{width:24px;margin-right:0.75rem;text-align:center;font-size:1.2rem}
.file-name{flex:1;word-break:break-all}
.file-size{color:#888;font-size:0.85rem;margin-left:1rem}
.file-actions{display:flex;gap:0.5rem}
.drop-zone{border:2px dashed #0f3460;border-radius:8px;padding:2rem;text-align:center;margin-bottom:1rem;transition:all 0.2s}
.drop-zone.drag-over{border-color:#00d4ff;background:rgba(0,212,255,0.1)}
.drop-zone input{display:none}
.modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.8);display:none;align-items:center;justify-content:center;z-index:100}
.modal.active{display:flex}
.modal-content{background:#16213e;padding:1.5rem;border-radius:8px;min-width:300px;max-width:90%}
.modal-content h2{margin-bottom:1rem;font-size:1.2rem}
.modal-content input{width:100%;padding:0.5rem;border:1px solid #0f3460;border-radius:4px;background:#1a1a2e;color:#fff;margin-bottom:1rem}
.modal-buttons{display:flex;gap:0.5rem;justify-content:flex-end}
.modal-editor{width:90vw;max-width:800px;height:80vh;display:flex;flex-direction:column}
.modal-editor h2{flex-shrink:0}
.modal-editor textarea{flex:1;width:100%;padding:0.75rem;border:1px solid #0f3460;border-radius:4px;background:#1a1a2e;color:#fff;font-family:monospace;font-size:0.9rem;resize:none;margin-bottom:1rem}
.modal-editor .modal-buttons{flex-shrink:0}
.progress{height:4px;background:#0f3460;border-radius:2px;margin-top:0.5rem;overflow:hidden;display:none}
.progress.active{display:block}
.progress-bar{height:100%;background:#00d4ff;width:0;transition:width 0.3s}
.toast{position:fixed;bottom:1rem;right:1rem;background:#0f3460;padding:1rem;border-radius:8px;max-width:300px;transform:translateY(100px);opacity:0;transition:all 0.3s}
.toast.active{transform:translateY(0);opacity:1}
.toast.error{background:#c0392b}
.empty{text-align:center;padding:2rem;color:#888}
@media(max-width:600px){.file-size{display:none}.toolbar{flex-direction:column}.btn{width:100%}}
</style>
</head>
<body>
<div class="container">
<h1>SD Card File Manager</h1>
<div class="status">
<div class="status-item"><span class="status-dot" id="sdStatus"></span><span id="sdText">SD Card</span></div>
<div class="status-item"><span class="status-dot" id="usbStatus"></span><span id="usbText">USB MSC</span></div>
</div>
<div class="drop-zone" id="dropZone">
<p>Dateien hier ablegen oder <label style="color:#00d4ff;cursor:pointer">ausw&auml;hlen<input type="file" id="fileInput" multiple></label></p>
<div class="progress" id="uploadProgress"><div class="progress-bar" id="uploadBar"></div></div>
</div>
<div class="toolbar">
<button class="btn" id="btnRefresh">Aktualisieren</button>
<button class="btn" id="btnNewFolder">Neuer Ordner</button>
<button class="btn" id="btnUp" disabled>Nach oben</button>
<button class="btn btn-danger" id="btnDelete" disabled>L&ouml;schen</button>
</div>
<div class="path-bar">Pfad: <span id="pathDisplay">/</span></div>
<div class="file-list" id="fileList"><div class="empty">Laden...</div></div>
</div>
<div class="modal" id="folderModal">
<div class="modal-content">
<h2>Neuer Ordner</h2>
<input type="text" id="folderName" placeholder="Ordnername">
<div class="modal-buttons">
<button class="btn" id="btnCancelFolder">Abbrechen</button>
<button class="btn" id="btnCreateFolder">Erstellen</button>
</div>
</div>
</div>
<div class="modal" id="editorModal">
<div class="modal-content modal-editor">
<h2 id="editorTitle">Datei bearbeiten</h2>
<textarea id="editorContent" spellcheck="false"></textarea>
<div class="modal-buttons">
<button class="btn" id="btnCancelEdit">Abbrechen</button>
<button class="btn" id="btnSaveEdit">Speichern</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
const API={
list:p=>fetch('/api/files/list?path='+encodeURIComponent(p)).then(r=>r.json()),
upload:(p,f)=>{const fd=new FormData();fd.append('file',f);return fetch('/api/files/upload?path='+encodeURIComponent(p),{method:'POST',body:fd})},
download:f=>'/api/files/download?file='+encodeURIComponent(f),
delete:f=>fetch('/api/files/delete?file='+encodeURIComponent(f),{method:'DELETE'}),
mkdir:p=>fetch('/api/files/mkdir?path='+encodeURIComponent(p),{method:'POST'}),
status:()=>fetch('/api/status').then(r=>r.json()),
read:f=>fetch('/api/files/read?file='+encodeURIComponent(f)).then(r=>r.ok?r.text():Promise.reject(r)),
write:(f,c)=>fetch('/api/files/write?file='+encodeURIComponent(f),{method:'POST',body:c,headers:{'Content-Type':'text/plain'}})
};
const EDITABLE=['.txt','.json','.html','.htm','.css','.js','.xml','.md','.cfg','.ini','.csv','.log'];
let currentPath='/',selected=null,editingFile=null;
const $=id=>document.getElementById(id);
function isEditable(name){const n=name.toLowerCase();return EDITABLE.some(e=>n.endsWith(e))}
function toast(msg,err){const t=$('toast');t.textContent=msg;t.className='toast active'+(err?' error':'');setTimeout(()=>t.className='toast',3000)}
function formatSize(b){if(b<1024)return b+' B';if(b<1024*1024)return(b/1024).toFixed(1)+' KB';return(b/(1024*1024)).toFixed(1)+' MB'}
function updatePath(){
const parts=currentPath.split('/').filter(Boolean);
let html='<span class="path-segment" data-path="/">/</span>';
let p='';
parts.forEach(part=>{p+='/'+part;html+=' / <span class="path-segment" data-path="'+p+'">'+part+'</span>'});
$('pathDisplay').innerHTML=html;
$('pathDisplay').querySelectorAll('.path-segment').forEach(el=>el.onclick=()=>{currentPath=el.dataset.path||'/';loadFiles()});
$('btnUp').disabled=currentPath==='/';
}
function getFilePath(name){return currentPath+(currentPath==='/'?'':'/')+name}
async function loadFiles(){
selected=null;$('btnDelete').disabled=true;
try{
const data=await API.list(currentPath);
if(!data.success){toast(data.error||'Fehler',true);$('fileList').innerHTML='<div class="empty">'+(data.error||'Fehler beim Laden')+'</div>';return}
updatePath();
if(!data.files||data.files.length===0){$('fileList').innerHTML='<div class="empty">Verzeichnis ist leer</div>';return}
$('fileList').innerHTML=data.files.map(f=>{
const fp=getFilePath(f.name);
const canEdit=!f.isDir&&isEditable(f.name);
return `<div class="file-item" data-name="${f.name}" data-dir="${f.isDir}">
<span class="file-icon">${f.isDir?'📁':'📄'}</span>
<span class="file-name">${f.name}</span>
<span class="file-size">${f.isDir?'':formatSize(f.size)}</span>
<div class="file-actions">
${canEdit?'<button class="btn btn-small" onclick="editFile(\''+fp.replace(/'/g,"\\'")+'\');event.stopPropagation()">✏️</button>':''}
${f.isDir?'':'<a class="btn btn-small" href="'+API.download(fp)+'" download>⬇️</a>'}
</div></div>`}).join('');
$('fileList').querySelectorAll('.file-item').forEach(el=>{
el.onclick=e=>{
if(e.target.tagName==='A'||e.target.tagName==='BUTTON')return;
if(el.dataset.dir==='true'){currentPath+=(currentPath==='/'?'':'/')+el.dataset.name;loadFiles()}
else{$('fileList').querySelectorAll('.file-item').forEach(i=>i.classList.remove('selected'));el.classList.add('selected');selected=el.dataset.name;$('btnDelete').disabled=false}
}});
}catch(e){toast('Netzwerkfehler',true);$('fileList').innerHTML='<div class="empty">Verbindungsfehler</div>'}}
async function editFile(path){
editingFile=path;
$('editorTitle').textContent='Bearbeiten: '+path.split('/').pop();
$('editorContent').value='Laden...';
$('editorModal').classList.add('active');
try{
const content=await API.read(path);
$('editorContent').value=content;
}catch(e){
$('editorContent').value='Fehler beim Laden der Datei';
toast('Fehler beim Laden',true);
}}
async function saveFile(){
if(!editingFile)return;
try{
const r=await API.write(editingFile,$('editorContent').value);
const j=await r.json();
if(j.success){toast('Gespeichert');$('editorModal').classList.remove('active');editingFile=null;loadFiles()}
else toast(j.error||'Fehler beim Speichern',true);
}catch(e){toast('Fehler beim Speichern',true)}}
async function updateStatus(){
try{const s=await API.status();
$('sdStatus').className='status-dot '+(s.sdMounted?'ok':'err');
$('sdText').textContent='SD: '+(s.sdMounted?'OK':'Nicht eingebunden');
$('usbStatus').className='status-dot '+(s.usbMscActive?'ok':'err');
$('usbText').textContent='USB: '+(s.usbMscActive?'Aktiv':'Inaktiv');
}catch(e){}}
async function uploadFiles(files){
const bar=$('uploadBar'),prog=$('uploadProgress');
prog.classList.add('active');
for(let i=0;i<files.length;i++){
bar.style.width=((i/files.length)*100)+'%';
try{
const r=await API.upload(currentPath,files[i]);
const j=await r.json();
if(!j.success)toast('Upload fehlgeschlagen: '+files[i].name,true);
}catch(e){toast('Upload fehlgeschlagen: '+files[i].name,true)}}
bar.style.width='100%';
setTimeout(()=>{prog.classList.remove('active');bar.style.width='0'},500);
toast(files.length+' Datei(en) hochgeladen');
loadFiles()}
$('btnRefresh').onclick=()=>{loadFiles();updateStatus()};
$('btnUp').onclick=()=>{if(currentPath!=='/'){const parts=currentPath.split('/').filter(Boolean);parts.pop();currentPath='/'+parts.join('/');loadFiles()}};
$('btnNewFolder').onclick=()=>$('folderModal').classList.add('active');
$('btnCancelFolder').onclick=()=>{$('folderModal').classList.remove('active');$('folderName').value=''};
$('btnCreateFolder').onclick=async()=>{
const name=$('folderName').value.trim();
if(!name){toast('Bitte Namen eingeben',true);return}
try{const r=await API.mkdir(currentPath+(currentPath==='/'?'':'/')+name);const j=await r.json();
if(j.success){toast('Ordner erstellt');loadFiles()}else toast(j.error||'Fehler',true);
}catch(e){toast('Fehler',true)}
$('folderModal').classList.remove('active');$('folderName').value=''};
$('btnDelete').onclick=async()=>{
if(!selected||!confirm('Wirklich löschen: '+selected+'?'))return;
try{const r=await API.delete(currentPath+(currentPath==='/'?'':'/')+selected);const j=await r.json();
if(j.success){toast('Gelöscht');loadFiles()}else toast(j.error||'Fehler',true);
}catch(e){toast('Fehler',true)}};
$('btnCancelEdit').onclick=()=>{$('editorModal').classList.remove('active');editingFile=null};
$('btnSaveEdit').onclick=saveFile;
$('folderName').onkeydown=e=>{if(e.key==='Enter')$('btnCreateFolder').click()};
$('editorContent').onkeydown=e=>{if(e.ctrlKey&&e.key==='s'){e.preventDefault();saveFile()}};
const dz=$('dropZone');
dz.ondragover=e=>{e.preventDefault();dz.classList.add('drag-over')};
dz.ondragleave=()=>dz.classList.remove('drag-over');
dz.ondrop=e=>{e.preventDefault();dz.classList.remove('drag-over');if(e.dataTransfer.files.length)uploadFiles(e.dataTransfer.files)};
$('fileInput').onchange=e=>{if(e.target.files.length)uploadFiles(e.target.files);e.target.value=''};
window.editFile=editFile;
loadFiles();updateStatus();setInterval(updateStatus,10000);
</script>
</body>
</html>