drop/public/index.html

35 lines
8.1 KiB
HTML
Raw 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="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>Drop</title>
<style>
:root{--hue:0;--saturation:0%;--app-background:#000;--surface:hsla(var(--hue),var(--saturation),10%,.8);--text:#eee;--muted:#969696;--border:#2c2c2c;--accent:hsl(var(--hue),var(--saturation),78%);--on-accent:#161616;--danger:#ffb4ab}*{box-sizing:border-box}body{margin:0;background:var(--app-background);color:var(--text);font:15px system-ui,sans-serif;min-height:100svh;display:grid;place-items:center;padding:24px}body.drag::after{content:'';position:fixed;inset:12px;border:1px dashed var(--accent);border-radius:16px;pointer-events:none}main{width:100%;max-width:400px}h1{font-size:22px;font-weight:550;margin:0 0 24px;letter-spacing:-.6px}button,input{font:inherit}button{cursor:pointer;border:0;min-height:48px;border-radius:12px;background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 20px}button:hover{filter:brightness(1.08)}button:active{transform:translateY(1px)}button:disabled{opacity:.4;cursor:default}input{min-height:48px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px;color:var(--text);width:100%}input[type=number]{appearance:textfield;-moz-appearance:textfield}input::-webkit-inner-spin-button,input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}:focus-visible{outline:2px solid var(--accent);outline-offset:4px}label{color:var(--muted);font-size:13px}#login,#upload{display:grid;gap:16px}#drop{background:var(--surface);border:1px solid var(--border);color:var(--text);width:100%;font-weight:500}#storage{margin:0;font-size:13px;color:var(--muted)}#filename{overflow-wrap:anywhere;margin:0;color:var(--muted);font-size:13px}small{font-size:12px;color:var(--muted);font-weight:400}.bottom{display:flex;align-items:end;gap:12px}.days{width:110px;display:grid;gap:8px}.bottom button{flex:1}.error{color:var(--danger);font-size:13px;margin:12px 0 0;min-height:18px}#result{position:relative}#result h2{font-size:20px;margin:0 48px 24px 0;font-weight:500}.close{position:absolute;right:-12px;top:-12px;background:transparent;color:var(--muted);padding:0;width:48px;font-size:26px}.copy{width:100%;margin-top:16px}#link{font-size:13px}#copied{font-size:13px;color:var(--muted);min-height:18px;margin-bottom:0}#busy{display:grid;place-content:center;text-align:center;gap:20px;min-height:180px}.spinner{width:32px;height:32px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;margin:auto}#progress{font-variant-numeric:tabular-nums;color:var(--muted)}[hidden]{display:none!important}@keyframes spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation:none}button:active{transform:none}}
</style>
<main>
<section id="entry" hidden><h1>Drop</h1><form id="login" hidden><label for="password">Пароль</label><input id="password" type="password" autocomplete="current-password" required><button>Войти</button></form><form id="upload" hidden><input type="file" id="file" hidden><button type="button" id="drop">Выбрать файл</button><p id="filename" aria-live="polite" hidden></p><div class="bottom"><div class="days"><label for="days">Срок, дней</label><input id="days" type="number" min="1" max="3650" step="1" value="1" required></div><button id="send" disabled>Upload</button></div><p id="storage" hidden></p></form><p class="error" id="error" role="alert"></p></section>
<section id="busy" hidden role="status" aria-live="polite" tabindex="-1"><div class="spinner"></div><span>Загружаем</span><span id="progress">0%</span></section>
<section id="result" hidden aria-labelledby="resultTitle"><button type="button" class="close" id="close" aria-label="Закрыть">×</button><h2 id="resultTitle" tabindex="-1">Ссылка готова</h2><input id="link" readonly aria-label="Ссылка на файл"><button type="button" id="copy" class="copy">Copy link</button><p id="copied" aria-live="polite"></p></section>
</main>
<script>
const $=id=>document.getElementById(id);let selected=null,uploading=false,authenticated=false,dragDepth=0;
function view(id){for(const name of ['entry','busy','result'])$(name).hidden=name!==id;document.body.classList.remove('drag');dragDepth=0}
function session(ok){authenticated=ok;if(ok)refreshStorage();else $('storage').hidden=true;$('login').hidden=ok;$('upload').hidden=!ok;view('entry');if(!ok)$('password').focus()}
async function refreshStorage(){
if(!authenticated)return;
try{const response=await fetch('/api/storage',{cache:'no-store'});if(!response.ok)throw Error();const space=await response.json();if(!authenticated)return;$('storage').textContent=`Свободное место - ${(space.free/1e9).toFixed(1)} / ${(space.total/1e9).toFixed(1)} GB`;$('storage').hidden=false}
catch{$('storage').hidden=true}
}
setInterval(refreshStorage,60000);
fetch('/api/session').then(r=>r.json()).then(r=>session(r.authenticated)).catch(()=>session(false));
$('login').onsubmit=async e=>{e.preventDefault();$('error').textContent='';try{const r=await fetch('/api/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:$('password').value})});const j=await r.json();if(!r.ok)throw Error(j.error);$('password').value='';session(true);$('drop').focus()}catch(e){$('error').textContent=e.message}};
function choose(f){if(uploading)return;$('error').textContent='';if(f&&f.size>4*1024**3){$('error').textContent='Максимальный размер 4 ГБ';return}selected=f||null;$('filename').hidden=!f;$('filename').textContent=f?f.name:'';$('send').disabled=!f}
$('drop').onclick=()=>$('file').click();$('file').onchange=()=>choose($('file').files[0]);
const canDrop=()=>authenticated&&!uploading&&!$('entry').hidden;
window.addEventListener('dragenter',e=>{e.preventDefault();if(canDrop()){dragDepth++;document.body.classList.add('drag')}});
window.addEventListener('dragover',e=>{e.preventDefault();if(e.dataTransfer)e.dataTransfer.dropEffect=canDrop()?'copy':'none'});
window.addEventListener('dragleave',e=>{e.preventDefault();if(--dragDepth<=0){dragDepth=0;document.body.classList.remove('drag')}});
window.addEventListener('drop',e=>{e.preventDefault();dragDepth=0;document.body.classList.remove('drag');if(canDrop()&&e.dataTransfer.files.length)choose(e.dataTransfer.files[0])});
window.addEventListener('dragend',()=>{dragDepth=0;document.body.classList.remove('drag')});
async function copy(){const value=$('link').value;try{if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(value)}else{$('link').focus();$('link').select();if(!document.execCommand('copy'))throw Error()}$('copied').textContent='Скопировано'}catch{$('copied').textContent='Нажмите Copy link или скопируйте ссылку вручную';$('link').focus();$('link').select()}}
$('upload').onsubmit=e=>{e.preventDefault();if(!selected||uploading)return;uploading=true;$('error').textContent='';view('busy');$('busy').focus();$('progress').textContent='0%';const xhr=new XMLHttpRequest();xhr.open('POST','/api/upload?days='+encodeURIComponent($('days').value));xhr.responseType='json';xhr.upload.onprogress=e=>{if(e.lengthComputable)$('progress').textContent=Math.floor(e.loaded/e.total*100)+'%'};xhr.onload=()=>{uploading=false;if(xhr.status!==200){view('entry');$('error').textContent=xhr.response?.error||'Не удалось загрузить файл';if(xhr.status===401)session(false);else $('send').focus();return}$('link').value=location.origin+xhr.response.path;$('copied').textContent='';view('result');$('resultTitle').focus();copy()};xhr.onerror=()=>{uploading=false;view('entry');$('error').textContent='Соединение прервано. Попробуйте ещё раз';$('send').focus()};const form=new FormData();form.append('file',selected);xhr.send(form)};
function reset(){if($('result').hidden)return;$('file').value='';choose(null);view('entry');$('drop').focus()}
$('copy').onclick=copy;$('close').onclick=reset;window.addEventListener('keydown',e=>{if(e.key==='Escape')reset()});
</script></html>