add pages

This commit is contained in:
2025-10-24 08:20:04 +02:00
parent 5a6b558d9c
commit 3645c60214
9 changed files with 943 additions and 0 deletions
+462
View File
@@ -0,0 +1,462 @@
{% extends "base.htm" %}
{% block title %}NanoShare - Upload{% endblock %}
{% block meta %}
<meta name="description" content="NanoShare is a lightweight self-hosted file sharing service built on Python Quart.">
<meta name="keywords" content="NanoShare, file sharing, self-hosted, Python Quart, open source">
<meta name="robots" content="index, follow" />
{% endblock %}
{% block head %}
<style>
/* ====== Design Tokens (match Files page) ====== */
:root{
--bg: #0f1221;
--panel: #161a2f;
--panel-2: #1b2140;
--text: #e7eaf6;
--muted: #a7b0d1;
--accent: #7c9eff;
--accent-2: #8ef6ff;
--border: rgba(255,255,255,.08);
--shadow: 0 10px 30px rgba(0,0,0,.25);
--radius: 14px;
--danger: #ff7a7a;
--success: #63e6b0;
}
@media (prefers-color-scheme: light){
:root{
--bg: #f6f7fb;
--panel: #ffffff;
--panel-2: #f3f6ff;
--text: #111321;
--muted: #495273;
--accent: #385cff;
--accent-2: #00bcd4;
--border: rgba(17,19,33,.08);
--shadow: 0 12px 24px rgba(17,19,33,.08);
--danger: #e74c3c;
--success: #0aa06e;
}
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
color:var(--text);
background:
radial-gradient(1200px 600px at 10% -10%, rgba(124,158,255,.18), transparent 60%),
radial-gradient(900px 500px at 110% 10%, rgba(142,246,255,.18), transparent 60%),
var(--bg);
font: 500 system-ui, -apple-system, Segoe UI, Roboto, Inter, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji";
}
.file-upload{
margin: clamp(16px, 3vw, 32px) auto;
}
.card{
background: linear-gradient(180deg, var(--panel), var(--panel-2));
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: saturate(120%) blur(6px);
-webkit-backdrop-filter: saturate(120%) blur(6px);
padding: clamp(18px, 2.6vw, 28px);
}
.page-title{
margin: 0 0 10px;
font-weight: 800;
letter-spacing:.2px;
font-size: clamp(22px, 2.4vw, 32px);
background: linear-gradient(90deg, var(--accent), var(--accent-2));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.subtle{color:var(--muted); margin:0 0 22px}
/* ====== Upload Zone ====== */
.dropzone{
position: relative;
border: 2px dashed color-mix(in srgb, var(--accent) 40%, var(--border));
border-radius: calc(var(--radius) - 4px);
background:
linear-gradient(180deg, color-mix(in srgb, var(--panel-2) 60%, transparent), transparent);
padding: clamp(22px, 4vw, 40px);
text-align: center;
cursor: pointer;
transition: border-color .25s ease, background .25s ease, transform .06s ease;
outline: none;
}
.dropzone:hover{
border-color: color-mix(in srgb, var(--accent) 65%, var(--border));
background: color-mix(in srgb, var(--panel-2) 75%, transparent);
}
.dropzone:focus-visible{
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}
.dropzone.drag{
border-color: var(--accent-2);
background: color-mix(in srgb, var(--accent) 12%, var(--panel-2));
}
.dz-icon{
font-size: 42px;
line-height:1;
margin-bottom: 10px;
filter: drop-shadow(0 6px 14px rgba(0,0,0,.25));
}
.dz-title{font-weight: 800; margin: 6px 0 2px}
.dz-hint{color:var(--muted); font-size:.95rem; margin:6px 0 0}
input[type="file"]{display:none}
/* ====== Fields ====== */
.field{margin-top:18px}
.label{
display:block; margin: 14px 0 8px; font-size:.92rem; color:var(--muted);
}
.input, .select, .textarea, .datetime{
width:100%;
border-radius: 12px;
border: 1px solid var(--border);
background: color-mix(in srgb, var(--panel) 88%, transparent);
color: var(--text);
padding: 12px 14px;
transition: border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
.textarea{ min-height: 120px; resize: vertical; }
.input:focus, .select:focus, .textarea:focus, .datetime:focus{
outline: none;
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
background: color-mix(in srgb, var(--panel-2) 92%, transparent);
}
.input[disabled], .textarea[disabled]{ opacity:.7; cursor:not-allowed; }
/* ====== Inline bits ====== */
.row{display:grid; grid-template-columns:1fr 1fr; gap:14px}
@media (max-width:720px){ .row{grid-template-columns:1fr} }
.badge{
display:inline-block; padding:.35rem .6rem; border-radius:999px; font-size:.8rem;
background: color-mix(in srgb, var(--accent) 16%, transparent);
border:1px solid color-mix(in srgb, var(--accent) 32%, var(--border));
color: var(--accent);
}
.preview{ margin-top: 10px; color:var(--muted) }
/* ====== Buttons ====== */
.actions{display:flex; align-items:center; gap:10px; margin-top:18px}
.btn{
--btn-br: color-mix(in srgb, var(--accent) 40%, var(--border));
display:inline-flex; align-items:center; justify-content:center; gap:.5ch;
padding:.8rem 1.1rem; border-radius:12px; border:1px solid var(--btn-br);
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 18%, transparent), transparent);
color:var(--text); font-weight:700; cursor:pointer;
transition: transform .06s ease, background .2s ease, border-color .2s ease, box-shadow .2s ease;
box-shadow: 0 2px 0 rgba(0,0,0,.08);
}
.btn:hover{
background: color-mix(in srgb, var(--accent) 30%, var(--panel-2));
border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
box-shadow: 0 6px 18px rgba(0,0,0,.18);
}
.btn:active{ transform: translateY(1px) scale(.98); }
.btn[disabled]{opacity:.55; cursor:not-allowed}
.btn-ghost{
background: transparent; border-color: var(--border);
}
/* ====== Progress ====== */
.progress{
height: 10px; border-radius: 999px; background: color-mix(in srgb, var(--panel-2) 70%, transparent);
border:1px solid var(--border); overflow:hidden; margin-top:12px;
}
.bar{
height:100%; width:0%;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
transition: width .2s ease;
}
/* ====== Toast ====== */
.toast{
position: fixed; right: 16px; bottom: 16px; z-index: 1000;
background: linear-gradient(180deg, var(--panel), var(--panel-2));
border:1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow);
color:var(--text); padding:10px 14px; display:none; align-items:center; gap:.6ch;
}
.toast.show{ display:flex; animation: pop .18s ease-out }
@keyframes pop{ from{ transform: translateY(8px); opacity:0 } to{ transform: translateY(0); opacity:1 } }
/* ====== Utility ====== */
.sr-only{ position:absolute!important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); border:0; }
</style>
{% endblock %}
{% block content %}
<main class="file-upload">
<section class="card" aria-labelledby="upload-title">
<h1 id="upload-title" class="page-title">Upload</h1>
<p class="subtle">Drop a file, paste from clipboard, or paste text. Configure expiry & add a note before uploading.</p>
<!-- Dropzone -->
<div id="dropzone" class="dropzone" tabindex="0" role="button" aria-label="Choose a file or drop it here">
<input type="file" id="fileInput" />
<div class="dz-icon">📤</div>
<div class="dz-title">Choose a file or drag it here</div>
<div class="dz-hint">Tip: Paste an image/file or text from clipboard (⌘/Ctrl + V)</div>
<div id="fileMeta" class="dz-hint" style="margin-top:8px;display:none;"></div>
</div>
<!-- Or paste text -->
<p class="subtle" style="text-align:center;margin:14px 0 0;">— or paste text below —</p>
<div class="field">
<label for="paste" class="label">Text</label>
<textarea id="paste" class="textarea" placeholder="Paste text…"></textarea>
</div>
<!-- Expiration & Note -->
<div class="row">
<div class="field">
<label for="expiresMode" class="label">Expiration</label>
<select id="expiresMode" class="select">
<option value="1h">1 hour</option>
<option value="24h">24 hours</option>
<option value="7d" selected>7 days</option>
<option value="30d">30 days</option>
<option value="never">Never</option>
<option value="custom">Custom date…</option>
</select>
</div>
<div class="field" id="customWrap" style="display:none;">
<label for="customExpire" class="label">Custom expiration</label>
<input type="datetime-local" id="customExpire" class="datetime" />
</div>
</div>
<div class="field">
<label for="note" class="label">Note (optional)</label>
<input id="note" class="input" type="text" placeholder="For Joe at ExampleCo" />
</div>
<!-- Actions -->
<div class="actions">
<button id="clearBtn" class="btn btn-ghost" type="button">🧹 Clear</button>
<div style="flex:1"></div>
<button id="uploadBtn" class="btn" disabled>⬆️ Upload</button>
</div>
<!-- Progress + Preview -->
<div class="progress" aria-hidden="true"><div class="bar" id="bar"></div></div>
<div id="preview" class="preview"></div>
</section>
</main>
<!-- Toast -->
<div id="toast" class="toast" role="status" aria-live="polite">✅ Uploaded successfully!</div>
<script>
/* ====== Elements ====== */
const dz = document.getElementById('dropzone');
const fileInput = document.getElementById('fileInput');
const pasteEl = document.getElementById('paste');
const uploadBtn = document.getElementById('uploadBtn');
const clearBtn = document.getElementById('clearBtn');
const preview = document.getElementById('preview');
const fileMeta = document.getElementById('fileMeta');
const expiresMode = document.getElementById('expiresMode');
const customWrap = document.getElementById('customWrap');
const customExpire = document.getElementById('customExpire');
const bar = document.getElementById('bar');
const toast = document.getElementById('toast');
let file=null, text='';
/* ====== Helpers ====== */
const mimeToExt = m => {
// --- Image types ---
if (m === 'image/png') return 'png';
if (m === 'image/jpeg') return 'jpg';
if (m === 'image/gif') return 'gif';
if (m === 'image/webp') return 'webp';
if (m === 'application/pdf') return 'pdf';
// --- Video MIME types ---
if (m === 'video/mp4') return 'mp4';
if (m === 'video/webm') return 'webm';
if (m === 'video/ogg') return 'ogv';
if (m === 'video/quicktime') return 'mov';
if (m === 'video/x-matroska' || m === 'video/mkv') return 'mkv';
if (m === 'video/x-msvideo') return 'avi';
if (m === 'video/x-ms-wmv') return 'wmv';
if (m === 'video/mpeg') return 'mpeg';
// --- Audio MIME types ---
if (m === 'audio/mpeg') return 'mp3'
if (m === 'audio/mp3') return 'mp3';
if (m === 'audio/aac') return 'aac';
if (m === 'audio/mp4' || m === 'audio/x-m4a') return 'm4a';
if (m === 'audio/ogg') return 'ogg';
if (m === 'audio/opus') return 'opus';
if (m === 'audio/webm') return 'weba';
if (m === 'audio/wav' || m === 'audio/x-wav') return 'wav';
if (m === 'audio/flac') return 'flac';
if (m === 'audio/x-ms-wma') return 'wma';
// --- Text fallback ---
if (m && m.startsWith('text/')) return 'txt';
return 'bin'; // default / unknown
};
const isoStamp = () => new Date().toISOString().replace(/[:]/g,'').replace(/\.\d+Z$/,'Z');
const fmtKB = b => `${Math.max(1, Math.round((b||0)/1024)).toLocaleString()} KB`;
function updateUI(){
uploadBtn.disabled = !(file || (text && text.trim()));
pasteEl.disabled = !!file;
fileInput.disabled = !!(text && text.trim());
// Preview + meta
if(file){
fileMeta.style.display='block';
fileMeta.innerHTML = `Selected: <span class="badge">${file.name}</span> · <span class="badge">${fmtKB(file.size)}</span>`;
}else{
fileMeta.style.display='none';
fileMeta.textContent='';
}
preview.textContent = '';
}
function setFileFromBlob(blob){
const ext=mimeToExt(blob.type||'');
const name=`pasted-${isoStamp()}.${ext}`;
file=new File([blob],name,{type:blob.type||'application/octet-stream'});
text=''; pasteEl.value='';
updateUI();
}
function clearAll(){
file=null; text=''; pasteEl.value='';
fileInput.value='';
bar.style.width='0%';
preview.textContent='';
updateUI();
}
/* ====== Dropzone interactions ====== */
dz.addEventListener('click',()=>!fileInput.disabled && fileInput.click());
dz.addEventListener('keydown',e=>{
if((e.key==='Enter'||e.key===' ') && !fileInput.disabled){ e.preventDefault(); fileInput.click(); }
});
dz.addEventListener('dragover',e=>{e.preventDefault(); dz.classList.add('drag');});
['dragleave','dragend'].forEach(ev=>dz.addEventListener(ev,()=>dz.classList.remove('drag')));
dz.addEventListener('drop',e=>{
e.preventDefault(); dz.classList.remove('drag');
if(fileInput.disabled)return;
const f=e.dataTransfer.files&&e.dataTransfer.files[0];
if(f){file=f;text='';pasteEl.value='';updateUI();}
});
fileInput.addEventListener('change',e=>{
const f=e.target.files[0]; if(f){file=f;text='';pasteEl.value='';updateUI();}
});
/* ====== Clipboard handling ====== */
window.addEventListener('paste',e=>{
if(file || (text && text.trim())) return;
const items=e.clipboardData?.items||[];
const fItem=[...items].find(it=>it.kind==='file');
if(fItem){
e.preventDefault();
const blob=fItem.getAsFile();
if(blob) setFileFromBlob(blob);
return;
}
const pasted=e.clipboardData?.getData('text')||'';
if(pasted){ pasteEl.value=pasted; text=pasted; updateUI(); }
});
pasteEl.addEventListener('input',e=>{ text = e.target.value; updateUI(); });
/* ====== Expiration handling ====== */
expiresMode.addEventListener('change',()=>{
if(expiresMode.value==='custom'){
customWrap.style.display='block';
customExpire.value = new Date(Date.now()+3600*1000).toISOString().slice(0,16);
} else {
customWrap.style.display='none';
}
});
/* ====== Clear ====== */
clearBtn.addEventListener('click',clearAll);
/* ====== Upload ====== */
uploadBtn.addEventListener('click',async()=>{
const note=document.getElementById('note').value;
let expiresValue=expiresMode.value;
if(expiresValue==='custom' && customExpire.value){
const dt=new Date(customExpire.value);
if(isNaN(+dt)){ preview.textContent='❌ Invalid custom date'; return; }
expiresValue=dt.toISOString();
}
const form=new FormData();
if(file) form.append('file',file,file.name);
if(text && text.trim()) form.append('text',text);
form.append('expires',expiresValue);
form.append('note',note);
uploadBtn.disabled=true;
preview.textContent='Uploading…';
bar.style.width='0%';
try{
// Use fetch with progress via XHR fallback for better granularity
const endpoint = '/api/upload';
if('upload' in new XMLHttpRequest()){
await new Promise((resolve, reject)=>{
const xhr=new XMLHttpRequest();
xhr.open('POST', endpoint);
xhr.upload.onprogress = (e)=>{
if(e.lengthComputable){
const pct = Math.round((e.loaded/e.total)*100);
bar.style.width = pct + '%';
}
};
xhr.onload = ()=> xhr.status>=200 && xhr.status<300 ? resolve() : reject(new Error('Upload failed'));
xhr.onerror = ()=> reject(new Error('Network error'));
xhr.send(form);
});
} else {
const res=await fetch(endpoint,{method:'POST',body:form});
if(!res.ok) throw new Error('Upload failed');
bar.style.width='100%';
}
preview.textContent = '✅ Uploaded successfully!';
showToast('✅ Uploaded successfully!');
// Optional: redirect to Files after short delay
// setTimeout(()=>{ window.location.href = '{{ url_for("side_main.files") }}'; }, 800);
}catch(err){
preview.textContent = '❌ ' + (err?.message || 'Upload error');
showToast('❌ Upload failed', true);
bar.style.width='0%';
}finally{
uploadBtn.disabled=false;
}
});
function showToast(msg, danger=false){
toast.textContent = msg;
toast.style.borderColor = danger ? 'color-mix(in srgb, var(--danger) 50%, var(--border))' : 'var(--border)';
toast.classList.add('show');
setTimeout(()=> toast.classList.remove('show'), 1800);
}
/* ====== Init ====== */
updateUI();
</script>
{% endblock %}