Files
dickendock/backend/upload-page.html

239 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>