diff --git a/backend/upload-page.html b/backend/upload-page.html
index 832db7d..bf129d6 100644
--- a/backend/upload-page.html
+++ b/backend/upload-page.html
@@ -28,8 +28,14 @@
.done-list li{padding:2px 0;list-style:none}
.done-list li::before{content:'β ';color:#4ecdc4}
#progress{height:4px;background:rgba(78,205,196,0.15);border-radius:2px;margin-top:10px;overflow:hidden}
- #progress-bar{height:100%;background:#4ecdc4;width:0;transition:width 0.3s}
+ #progress-bar{height:100%;background:#4ecdc4;width:0;transition:width 0.15s}
+ #progress-label{font-size:11px;color:rgba(255,255,255,0.4);margin-top:4px;font-family:monospace;min-height:14px}
.attempts{color:#ffe66d;font-size:11px;margin-top:6px;font-family:monospace}
+ #drop-zone{border:2px dashed rgba(255,255,255,0.15);border-radius:10px;padding:18px 12px;text-align:center;
+ color:rgba(255,255,255,0.45);font-size:12px;cursor:pointer;transition:all 0.15s;margin-bottom:4px}
+ #drop-zone.drag{border-color:#4ecdc4;background:rgba(78,205,196,0.08);color:#4ecdc4}
+ #file-input{display:none}
+ #file-label{display:block;font-size:12px;color:rgba(255,255,255,0.55);margin-top:6px;word-break:break-all}
@@ -49,8 +55,11 @@
+
π Dateien hierher ziehen oder klicken
+
+
@@ -128,13 +137,66 @@ async function verify() {
}
}
+// ββ Drag & Drop ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+const dropZone = document.getElementById('drop-zone');
+const fileInput = document.getElementById('file-input');
+
+dropZone.addEventListener('click', () => fileInput.click());
+dropZone.addEventListener('dragover', e => { e.preventDefault(); dropZone.classList.add('drag'); });
+dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag'));
+dropZone.addEventListener('drop', e => {
+ e.preventDefault();
+ dropZone.classList.remove('drag');
+ if (e.dataTransfer.files.length) {
+ fileInput.files = e.dataTransfer.files;
+ updateFileLabel();
+ }
+});
+fileInput.addEventListener('change', updateFileLabel);
+
+function updateFileLabel() {
+ const files = fileInput.files;
+ const label = document.getElementById('file-label');
+ if (!files.length) { label.textContent = ''; return; }
+ label.textContent = files.length === 1
+ ? files[0].name
+ : files.length + ' Dateien ausgewΓ€hlt';
+}
+
+// ββ Upload mit echtem Byte-Fortschritt (XMLHttpRequest statt fetch,
+// da fetch() keine Upload-Progress-Events liefert) βββββββββββββββββββββββββ
+function uploadOne(file) {
+ return new Promise((resolve, reject) => {
+ const fd = new FormData();
+ fd.append('file', file);
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', '/api/upload-shares/public/' + encodeURIComponent(token) + '/upload');
+ xhr.setRequestHeader('x-upload-password', password);
+ xhr.upload.onprogress = e => {
+ if (e.lengthComputable) setProgress(e.loaded / e.total * 100, file.name);
+ };
+ xhr.onload = () => {
+ let d = {};
+ try { d = JSON.parse(xhr.responseText); } catch {}
+ if (xhr.status >= 200 && xhr.status < 300) resolve(d);
+ else reject(new Error(d.error || 'Upload fehlgeschlagen'));
+ };
+ xhr.onerror = () => reject(new Error('Verbindung fehlgeschlagen'));
+ xhr.send(fd);
+ });
+}
+
+function setProgress(percent, label) {
+ document.getElementById('progress-bar').style.width = percent + '%';
+ document.getElementById('progress-label').textContent = label ? Math.round(percent) + '% Β· ' + label : '';
+}
+
async function uploadFiles() {
const files = document.getElementById('file-input').files;
if (!files.length) { document.getElementById('file-input').click(); return; }
const btn = document.getElementById('upload-btn');
btn.disabled = true;
const msg = document.getElementById('upload-msg');
- const bar = document.getElementById('progress-bar');
const list = document.getElementById('done-list');
msg.classList.add('hidden');
@@ -146,33 +208,20 @@ async function uploadFiles() {
msg.classList.remove('hidden');
continue;
}
- bar.style.width = ((i + 0.5) / files.length * 100) + '%';
- const fd = new FormData();
- fd.append('file', file);
+ setProgress(0, file.name);
try {
- const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/upload', {
- method: 'POST',
- headers: { 'x-upload-password': password },
- body: fd
- });
- const d = await r.json();
- if (r.ok) {
- const li = document.createElement('li');
- li.textContent = file.name;
- list.appendChild(li);
- } else {
- msg.className = 'msg err';
- msg.textContent = d.error;
- msg.classList.remove('hidden');
- }
+ await uploadOne(file);
+ setProgress(100, file.name);
+ const li = document.createElement('li');
+ li.textContent = file.name;
+ list.appendChild(li);
} catch(e) {
msg.className = 'msg err';
- msg.textContent = 'Upload fehlgeschlagen: ' + e.message;
+ msg.textContent = file.name + ': ' + e.message;
msg.classList.remove('hidden');
}
}
- bar.style.width = '100%';
- setTimeout(() => bar.style.width = '0', 1500);
+ setTimeout(() => setProgress(0, ''), 1200);
if (list.children.length > 0) {
msg.className = 'msg ok';
msg.textContent = list.children.length + ' Datei' + (list.children.length !== 1 ? 'en' : '') + ' erfolgreich hochgeladen';
@@ -180,6 +229,7 @@ async function uploadFiles() {
}
btn.disabled = false;
document.getElementById('file-input').value = '';
+ updateFileLabel();
}
init();
diff --git a/frontend/src/lib.js b/frontend/src/lib.js
index 66788ea..b0c409b 100644
--- a/frontend/src/lib.js
+++ b/frontend/src/lib.js
@@ -15,6 +15,35 @@ export const api = async (path, { body, method, isFile } = {}) => {
return data;
};
+// ββ Upload mit echtem Byte-Fortschritt ββββββββββββββββββββββββββββββββββββββ
+// fetch() liefert keinerlei Upload-Progress-Events, daher hier bewusst auf
+// XMLHttpRequest aufgebaut. Nur fΓΌr Datei-Uploads gedacht (FormData-Body).
+// onProgress(percent: 0-100) wird wΓ€hrend des Uploads laufend aufgerufen.
+export const apiUpload = (path, formData, { onProgress } = {}) => {
+ const token = localStorage.getItem('sk_token');
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', `/api${path}`);
+ if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
+
+ xhr.upload.onprogress = e => {
+ if (e.lengthComputable && onProgress) {
+ onProgress(Math.round((e.loaded / e.total) * 100));
+ }
+ };
+
+ xhr.onload = () => {
+ let data = {};
+ try { data = JSON.parse(xhr.responseText); } catch { /* leere/ungΓΌltige Antwort */ }
+ if (xhr.status >= 200 && xhr.status < 300) resolve(data);
+ else reject(new Error(data.error || 'Fehler'));
+ };
+
+ xhr.onerror = () => reject(new Error('Verbindung fehlgeschlagen'));
+ xhr.send(formData);
+ });
+};
+
// ββ Design Tokens βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Einheitliche Styles fΓΌr alle Tools β in jedem Tool importierbar
export const S = {
diff --git a/frontend/src/tools/dateien.jsx b/frontend/src/tools/dateien.jsx
index a48c340..f6ec69a 100644
--- a/frontend/src/tools/dateien.jsx
+++ b/frontend/src/tools/dateien.jsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback } from 'react';
-import { api, S } from '../lib.js';
+import { api, apiUpload, S } from '../lib.js';
import { useConfirm } from '../confirm.jsx';
import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, ChevronIcon } from '../icons.jsx';
@@ -602,6 +602,7 @@ export default function Dateien({ toast, mobile }) {
});
const [pwPrompt, setPwPrompt] = useState(null); // {file}
const [uploading, setUploading] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(0);
const [dragOver, setDragOver] = useState(false);
const [newFolder, setNewFolder] = useState(false);
const [folderName, setFolderName] = useState('');
@@ -634,13 +635,14 @@ export default function Dateien({ toast, mobile }) {
const doUpload = async file => {
if (!file) return;
setUploading(true);
+ setUploadProgress(0);
try {
const form = new FormData();
form.append('file', file);
if (currentFolderId) form.append('folder_id', currentFolderId);
- await api('/tools/dateien',{method:'POST',body:form,isFile:true});
+ await apiUpload('/tools/dateien', form, { onProgress: setUploadProgress });
toast(`${file.name} hochgeladen β`); load();
- } catch(e) { toast(e.message,'error'); } finally { setUploading(false); }
+ } catch(e) { toast(e.message,'error'); } finally { setUploading(false); setUploadProgress(0); }
};
const onFileInput = e => { doUpload(e.target.files?.[0]); e.target.value=''; };
@@ -912,6 +914,7 @@ export default function Dateien({ toast, mobile }) {
{e.preventDefault();setDragOver(true);}} onDragLeave={()=>setDragOver(false)}
onDrop={onDrop} onClick={()=>fileRef.current?.click()}
style={{
+ position:'relative',overflow:'hidden',
display:'flex',alignItems:'center',justifyContent:'center',gap:8,
padding:'0 18px',height:44,borderRadius:10,cursor:'pointer',
border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.15)'}`,
@@ -919,9 +922,14 @@ export default function Dateien({ toast, mobile }) {
transition:'all 0.15s',flexShrink:0,
minWidth:mobile?'100%':200,
}}>
+ {uploading && (
+
+ )}
-
- {uploading?'Hochladenβ¦':dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
+
+ {uploading?`Hochladen⦠${uploadProgress}%`:dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
}