239 lines
11 KiB
HTML
239 lines
11 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Datei hochladen – DickenDock</title>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
body{background:#0d0d0f;color:#fff;font-family:'Courier New',monospace;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
|
||
.card{background:#1a1a1e;border:1px solid rgba(255,255,255,0.1);border-radius:16px;padding:28px;width:100%;max-width:440px}
|
||
h1{font-size:18px;margin-bottom:6px;color:#4ecdc4}
|
||
.sub{color:rgba(255,255,255,0.4);font-size:12px;margin-bottom:20px}
|
||
label{display:block;color:rgba(255,255,255,0.5);font-size:10px;letter-spacing:2px;margin-bottom:4px;margin-top:12px}
|
||
input[type=password],input[type=file]{width:100%;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.12);border-radius:8px;padding:10px 12px;color:#fff;font-family:inherit;font-size:14px;outline:none;transition:border 0.2s}
|
||
input[type=password]:focus{border-color:#4ecdc4}
|
||
input[type=file]{cursor:pointer;padding:8px}
|
||
input[type=file]::file-selector-button{background:#4ecdc4;border:none;border-radius:6px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-weight:700;padding:4px 12px;margin-right:10px}
|
||
.btn{width:100%;background:#4ecdc4;border:none;border-radius:8px;color:#0d0d0f;cursor:pointer;font-family:inherit;font-size:14px;font-weight:700;padding:12px;margin-top:12px;transition:opacity 0.2s}
|
||
.btn:hover:not(:disabled){opacity:0.85}
|
||
.btn:disabled{opacity:0.4;cursor:default}
|
||
.info{background:rgba(78,205,196,0.08);border:1px solid rgba(78,205,196,0.2);border-radius:8px;padding:10px 12px;font-size:12px;color:rgba(255,255,255,0.6);margin-bottom:16px;margin-top:4px}
|
||
.msg{border-radius:8px;padding:10px 12px;font-size:13px;margin-top:10px}
|
||
.msg.ok{background:rgba(78,205,196,0.12);border:1px solid rgba(78,205,196,0.3);color:#4ecdc4}
|
||
.msg.err{background:rgba(255,107,157,0.12);border:1px solid rgba(255,107,157,0.3);color:#ff6b9d}
|
||
.msg.warn{background:rgba(255,230,109,0.12);border:1px solid rgba(255,230,109,0.3);color:#ffe66d}
|
||
.hidden{display:none}
|
||
.done-list{margin-top:10px;font-size:12px;color:rgba(255,255,255,0.5)}
|
||
.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.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}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="card">
|
||
<h1>📤 Datei hochladen</h1>
|
||
<p class="sub" id="sub">Dateien senden</p>
|
||
|
||
<div id="pw-section">
|
||
<div class="info" id="share-info">⏳ Link wird geprüft…</div>
|
||
<label>PASSWORT</label>
|
||
<input type="password" id="pw" placeholder="Passwort eingeben" autocomplete="off" onkeydown="if(event.key==='Enter')verify()"/>
|
||
<button class="btn" id="pw-btn" onclick="verify()">Zugang bestätigen</button>
|
||
<div id="pw-msg" class="hidden"></div>
|
||
<div id="attempts-info" class="attempts hidden"></div>
|
||
</div>
|
||
|
||
<div id="upload-section" class="hidden">
|
||
<div class="info" id="upload-info"></div>
|
||
<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/>
|
||
<div id="progress"><div id="progress-bar"></div></div>
|
||
<div id="progress-label"></div>
|
||
<button class="btn" id="upload-btn" onclick="uploadFiles()">⬆ Hochladen</button>
|
||
<div id="upload-msg" class="hidden"></div>
|
||
<ul class="done-list" id="done-list"></ul>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const token = location.pathname.split('/u/')[1] || '';
|
||
let password = '';
|
||
let maxMb = 10;
|
||
|
||
async function init() {
|
||
if (!token) return showFatal('Ungültiger Link-Aufbau');
|
||
try {
|
||
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token));
|
||
const d = await r.json();
|
||
if (!r.ok) return showFatal(d.error || 'Link ungültig');
|
||
maxMb = d.max_size_mb;
|
||
const exp = new Date(d.expires_at).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||
document.getElementById('share-info').textContent = '🔐 Gültig bis ' + exp + ' · max ' + d.max_size_mb + ' MB pro Datei';
|
||
} catch(e) {
|
||
showFatal('Verbindung fehlgeschlagen: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function showFatal(msg) {
|
||
document.getElementById('pw-section').innerHTML = '<div class="msg err">' + msg + '</div>';
|
||
}
|
||
|
||
async function verify() {
|
||
const pw = document.getElementById('pw').value;
|
||
if (!pw) { document.getElementById('pw').focus(); return; }
|
||
const btn = document.getElementById('pw-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = '…';
|
||
const msg = document.getElementById('pw-msg');
|
||
try {
|
||
const r = await fetch('/api/upload-shares/public/' + encodeURIComponent(token) + '/verify', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ password: pw })
|
||
});
|
||
const d = await r.json();
|
||
if (!r.ok) {
|
||
msg.className = 'msg err';
|
||
msg.textContent = d.error;
|
||
msg.classList.remove('hidden');
|
||
if (d.locked) {
|
||
showFatal(d.error);
|
||
return;
|
||
}
|
||
if (d.attempts_left !== undefined) {
|
||
const ai = document.getElementById('attempts-info');
|
||
ai.textContent = '⚠ Noch ' + d.attempts_left + ' Versuch' + (d.attempts_left===1?'':'e') + ' übrig';
|
||
ai.classList.remove('hidden');
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = 'Zugang bestätigen';
|
||
return;
|
||
}
|
||
password = pw;
|
||
maxMb = d.max_size_mb;
|
||
document.getElementById('pw-section').classList.add('hidden');
|
||
const us = document.getElementById('upload-section');
|
||
us.classList.remove('hidden');
|
||
const exp = new Date(d.expires_at).toLocaleString('de-DE',{timeZone:'Europe/Berlin',day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
|
||
document.getElementById('upload-info').textContent = '✓ Zugang bestätigt · Gültig bis ' + exp + ' · max ' + d.max_size_mb + ' MB pro Datei';
|
||
document.getElementById('sub').textContent = 'Bereit zum Hochladen';
|
||
} catch(e) {
|
||
msg.className = 'msg err';
|
||
msg.textContent = 'Verbindung fehlgeschlagen: ' + e.message;
|
||
msg.classList.remove('hidden');
|
||
btn.disabled = false;
|
||
btn.textContent = 'Zugang bestätigen';
|
||
}
|
||
}
|
||
|
||
// ── 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 list = document.getElementById('done-list');
|
||
msg.classList.add('hidden');
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
if (file.size > maxMb * 1024 * 1024) {
|
||
msg.className = 'msg warn';
|
||
msg.textContent = '"' + file.name + '" ist zu groß (max ' + maxMb + ' MB)';
|
||
msg.classList.remove('hidden');
|
||
continue;
|
||
}
|
||
setProgress(0, file.name);
|
||
try {
|
||
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 = file.name + ': ' + e.message;
|
||
msg.classList.remove('hidden');
|
||
}
|
||
}
|
||
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';
|
||
msg.classList.remove('hidden');
|
||
}
|
||
btn.disabled = false;
|
||
document.getElementById('file-input').value = '';
|
||
updateFileLabel();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|