Fortschrittsanzeige beim Upload (Dateien-Tool + öffentliche Upload-Seite), Drag&Drop auf Upload-Seite ergänzt
This commit is contained in:
@@ -28,8 +28,14 @@
|
|||||||
.done-list li{padding:2px 0;list-style:none}
|
.done-list li{padding:2px 0;list-style:none}
|
||||||
.done-list li::before{content:'✓ ';color:#4ecdc4}
|
.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{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}
|
.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}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -49,8 +55,11 @@
|
|||||||
<div id="upload-section" class="hidden">
|
<div id="upload-section" class="hidden">
|
||||||
<div class="info" id="upload-info"></div>
|
<div class="info" id="upload-info"></div>
|
||||||
<label>DATEI(EN) AUSWÄHLEN</label>
|
<label>DATEI(EN) AUSWÄHLEN</label>
|
||||||
|
<div id="drop-zone">📁 Dateien hierher ziehen oder klicken</div>
|
||||||
|
<div id="file-label"></div>
|
||||||
<input type="file" id="file-input" multiple/>
|
<input type="file" id="file-input" multiple/>
|
||||||
<div id="progress"><div id="progress-bar"></div></div>
|
<div id="progress"><div id="progress-bar"></div></div>
|
||||||
|
<div id="progress-label"></div>
|
||||||
<button class="btn" id="upload-btn" onclick="uploadFiles()">⬆ Hochladen</button>
|
<button class="btn" id="upload-btn" onclick="uploadFiles()">⬆ Hochladen</button>
|
||||||
<div id="upload-msg" class="hidden"></div>
|
<div id="upload-msg" class="hidden"></div>
|
||||||
<ul class="done-list" id="done-list"></ul>
|
<ul class="done-list" id="done-list"></ul>
|
||||||
@@ -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() {
|
async function uploadFiles() {
|
||||||
const files = document.getElementById('file-input').files;
|
const files = document.getElementById('file-input').files;
|
||||||
if (!files.length) { document.getElementById('file-input').click(); return; }
|
if (!files.length) { document.getElementById('file-input').click(); return; }
|
||||||
const btn = document.getElementById('upload-btn');
|
const btn = document.getElementById('upload-btn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const msg = document.getElementById('upload-msg');
|
const msg = document.getElementById('upload-msg');
|
||||||
const bar = document.getElementById('progress-bar');
|
|
||||||
const list = document.getElementById('done-list');
|
const list = document.getElementById('done-list');
|
||||||
msg.classList.add('hidden');
|
msg.classList.add('hidden');
|
||||||
|
|
||||||
@@ -146,33 +208,20 @@ async function uploadFiles() {
|
|||||||
msg.classList.remove('hidden');
|
msg.classList.remove('hidden');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bar.style.width = ((i + 0.5) / files.length * 100) + '%';
|
setProgress(0, file.name);
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('file', file);
|
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/upload', {
|
await uploadOne(file);
|
||||||
method: 'POST',
|
setProgress(100, file.name);
|
||||||
headers: { 'x-upload-password': password },
|
const li = document.createElement('li');
|
||||||
body: fd
|
li.textContent = file.name;
|
||||||
});
|
list.appendChild(li);
|
||||||
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');
|
|
||||||
}
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
msg.className = 'msg err';
|
msg.className = 'msg err';
|
||||||
msg.textContent = 'Upload fehlgeschlagen: ' + e.message;
|
msg.textContent = file.name + ': ' + e.message;
|
||||||
msg.classList.remove('hidden');
|
msg.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bar.style.width = '100%';
|
setTimeout(() => setProgress(0, ''), 1200);
|
||||||
setTimeout(() => bar.style.width = '0', 1500);
|
|
||||||
if (list.children.length > 0) {
|
if (list.children.length > 0) {
|
||||||
msg.className = 'msg ok';
|
msg.className = 'msg ok';
|
||||||
msg.textContent = list.children.length + ' Datei' + (list.children.length !== 1 ? 'en' : '') + ' erfolgreich hochgeladen';
|
msg.textContent = list.children.length + ' Datei' + (list.children.length !== 1 ? 'en' : '') + ' erfolgreich hochgeladen';
|
||||||
@@ -180,6 +229,7 @@ async function uploadFiles() {
|
|||||||
}
|
}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
document.getElementById('file-input').value = '';
|
document.getElementById('file-input').value = '';
|
||||||
|
updateFileLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -15,6 +15,35 @@ export const api = async (path, { body, method, isFile } = {}) => {
|
|||||||
return data;
|
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 ─────────────────────────────────────────────────────────────
|
// ── Design Tokens ─────────────────────────────────────────────────────────────
|
||||||
// Einheitliche Styles für alle Tools – in jedem Tool importierbar
|
// Einheitliche Styles für alle Tools – in jedem Tool importierbar
|
||||||
export const S = {
|
export const S = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
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 { useConfirm } from '../confirm.jsx';
|
||||||
import { UploadIcon, DownloadIcon, TrashIcon, SearchIcon, ChevronIcon } from '../icons.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 [pwPrompt, setPwPrompt] = useState(null); // {file}
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const [newFolder, setNewFolder] = useState(false);
|
const [newFolder, setNewFolder] = useState(false);
|
||||||
const [folderName, setFolderName] = useState('');
|
const [folderName, setFolderName] = useState('');
|
||||||
@@ -634,13 +635,14 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
const doUpload = async file => {
|
const doUpload = async file => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
try {
|
try {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('file', file);
|
form.append('file', file);
|
||||||
if (currentFolderId) form.append('folder_id', currentFolderId);
|
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();
|
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=''; };
|
const onFileInput = e => { doUpload(e.target.files?.[0]); e.target.value=''; };
|
||||||
@@ -912,6 +914,7 @@ export default function Dateien({ toast, mobile }) {
|
|||||||
<div onDragOver={e=>{e.preventDefault();setDragOver(true);}} onDragLeave={()=>setDragOver(false)}
|
<div onDragOver={e=>{e.preventDefault();setDragOver(true);}} onDragLeave={()=>setDragOver(false)}
|
||||||
onDrop={onDrop} onClick={()=>fileRef.current?.click()}
|
onDrop={onDrop} onClick={()=>fileRef.current?.click()}
|
||||||
style={{
|
style={{
|
||||||
|
position:'relative',overflow:'hidden',
|
||||||
display:'flex',alignItems:'center',justifyContent:'center',gap:8,
|
display:'flex',alignItems:'center',justifyContent:'center',gap:8,
|
||||||
padding:'0 18px',height:44,borderRadius:10,cursor:'pointer',
|
padding:'0 18px',height:44,borderRadius:10,cursor:'pointer',
|
||||||
border:`2px dashed ${dragOver?'#4ecdc4':'rgba(255,255,255,0.15)'}`,
|
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,
|
transition:'all 0.15s',flexShrink:0,
|
||||||
minWidth:mobile?'100%':200,
|
minWidth:mobile?'100%':200,
|
||||||
}}>
|
}}>
|
||||||
|
{uploading && (
|
||||||
|
<div style={{position:'absolute',left:0,top:0,bottom:0,
|
||||||
|
width:`${uploadProgress}%`,background:'rgba(78,205,196,0.18)',
|
||||||
|
transition:'width 0.2s',pointerEvents:'none'}}/>
|
||||||
|
)}
|
||||||
<UploadIcon size={14} color={dragOver?'#4ecdc4':'rgba(255,255,255,0.4)'}/>
|
<UploadIcon size={14} color={dragOver?'#4ecdc4':'rgba(255,255,255,0.4)'}/>
|
||||||
<span style={{color:dragOver?'#4ecdc4':'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}>
|
<span style={{position:'relative',color:dragOver?'#4ecdc4':'rgba(255,255,255,0.45)',fontFamily:'monospace',fontSize:11}}>
|
||||||
{uploading?'Hochladen…':dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
|
{uploading?`Hochladen… ${uploadProgress}%`:dragOver?'Loslassen':mobile?'Datei hochladen':'Hier ablegen oder klicken'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|||||||
Reference in New Issue
Block a user