223 lines
12 KiB
HTML
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ä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ö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>
|