add pages
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
{% extends "base.htm" %}
|
||||
|
||||
{% block title %}NanoShare - Files{% 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 ====== */
|
||||
: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;
|
||||
}
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== Base ====== */
|
||||
*{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";
|
||||
}
|
||||
|
||||
/* ====== Layout ====== */
|
||||
.file-list{
|
||||
padding: clamp(16px, 3vw, 28px);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* ====== Header ====== */
|
||||
.page-title{
|
||||
margin: 0 0 18px;
|
||||
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);
|
||||
font-size: .95rem;
|
||||
margin: 0 0 22px;
|
||||
}
|
||||
|
||||
/* ====== Table Wrapper ====== */
|
||||
.table-wrap{
|
||||
overflow: auto;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.table{
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: transparent;
|
||||
}
|
||||
.table thead th{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
text-align: left;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .3px;
|
||||
padding: 12px 14px;
|
||||
color: var(--muted);
|
||||
background: color-mix(in srgb, var(--panel) 86%, black 0%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.table tbody td{
|
||||
padding: 14px;
|
||||
border-bottom: 1px dashed var(--border);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table tbody tr{
|
||||
transition: background .25s ease, transform .06s ease;
|
||||
}
|
||||
.table tbody tr:hover{
|
||||
background: color-mix(in srgb, var(--panel-2) 88%, transparent);
|
||||
}
|
||||
.table .cell--name a{
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
.table .cell--name a:hover{
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.table .cell--right{ text-align: right; }
|
||||
|
||||
/* Zebra for readability on dense lists */
|
||||
.table tbody tr:nth-child(2n){
|
||||
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
||||
}
|
||||
.table tbody tr:hover:nth-child(2n){
|
||||
background: color-mix(in srgb, var(--panel-2) 86%, transparent);
|
||||
}
|
||||
|
||||
/* ====== Badges (e.g., file size) ====== */
|
||||
.badge{
|
||||
display:inline-block;
|
||||
padding: .25rem .5rem;
|
||||
border-radius: 999px;
|
||||
font-size: .78rem;
|
||||
line-height: 1;
|
||||
background: color-mix(in srgb, var(--accent) 16%, transparent);
|
||||
color: var(--accent);
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--border));
|
||||
}
|
||||
|
||||
/* ====== Icon Buttons ====== */
|
||||
.actions{ display:flex; justify-content:flex-end; gap:8px; }
|
||||
.icon-btn{
|
||||
--btn-bg: color-mix(in srgb, var(--panel-2) 70%, transparent);
|
||||
--btn-br: color-mix(in srgb, var(--accent) 22%, var(--border));
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:.4ch;
|
||||
padding: .45rem .6rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--btn-br);
|
||||
background: var(--btn-bg);
|
||||
color: var(--text);
|
||||
font-size: .9rem;
|
||||
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);
|
||||
user-select: none;
|
||||
}
|
||||
.icon-btn:hover{
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--panel-2));
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,.18);
|
||||
}
|
||||
.icon-btn:active{ transform: translateY(1px) scale(.98); }
|
||||
.icon-btn[data-variant="ghost"]{
|
||||
background: transparent;
|
||||
border-color: var(--border);
|
||||
}
|
||||
.icon-btn[data-variant="primary"]{
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 22%, transparent), transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 50%, var(--border));
|
||||
}
|
||||
|
||||
/* ====== Small screens: wrap long text and stack actions ====== */
|
||||
@media (max-width: 720px){
|
||||
.table tbody td{ white-space: normal; }
|
||||
.actions{ flex-wrap: wrap; }
|
||||
.badge{ margin-top: 4px; }
|
||||
}
|
||||
|
||||
/* ====== 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-list">
|
||||
<section class="card" style="padding: clamp(18px, 2.6vw, 28px);">
|
||||
<h2 class="page-title">Files</h2>
|
||||
<p class="subtle">Your uploaded files at a glance. Click a filename to open, or use the actions on the right.</p>
|
||||
|
||||
<div class="table-wrap" role="region" aria-label="Files table" tabindex="0">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Filename</th>
|
||||
<th scope="col">Note</th>
|
||||
<th scope="col">Size</th>
|
||||
<th scope="col">Uploaded</th>
|
||||
<th scope="col">Expires</th>
|
||||
<th scope="col" class="cell--right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for file in files %}
|
||||
<tr>
|
||||
<td class="cell--name">
|
||||
<a href="{{ url_for('side_main.serve_file', file_id=file.file_id) }}">{{ file.file_name }}</a>
|
||||
</td>
|
||||
<td>{{ file.note }}</td>
|
||||
<td><span class="badge">{{ file.file_size }}</span></td>
|
||||
<td><time datetime="{{ file.uploaded_at }}">{{ file.uploaded_at }}</time></td>
|
||||
<td><time datetime="{{ file.expires_at }}">{{ file.expires_at }}</time></td>
|
||||
<td class="cell--right">
|
||||
<div class="actions">
|
||||
<button class="icon-btn" title="Info" data-action="info" aria-label="Info for {{ file.file_name }}">ℹ️ <span class="sr-only">Info</span></button>
|
||||
<button class="icon-btn" data-variant="primary" title="Edit" data-action="edit" aria-label="Edit {{ file.file_name }}">✏️ <span class="sr-only">Edit</span></button>
|
||||
<button class="icon-btn" data-variant="ghost" title="Copy link" data-action="copy" aria-label="Copy link to {{ file.file_name }}">📋 <span class="sr-only">Copy</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Minimal progressive enhancement for "Copy" action
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button[data-action="copy"]');
|
||||
if(!btn) return;
|
||||
const row = btn.closest('tr');
|
||||
const link = row?.querySelector('.cell--name a')?.href;
|
||||
if(!link) return;
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = '✅';
|
||||
setTimeout(() => btn.innerHTML = original, 1200);
|
||||
}).catch(() => {
|
||||
alert('Could not copy link.');
|
||||
});
|
||||
});
|
||||
|
||||
// Optional: placeholder handlers for info/edit hooks
|
||||
document.addEventListener('click', (e) => {
|
||||
const info = e.target.closest('button[data-action="info"]');
|
||||
const edit = e.target.closest('button[data-action="edit"]');
|
||||
if(info){
|
||||
const name = info.closest('tr')?.querySelector('.cell--name a')?.textContent?.trim();
|
||||
// Hook into your modal/toast here
|
||||
console.log('Info for:', name);
|
||||
}
|
||||
if(edit){
|
||||
const id = edit.closest('tr')?.querySelector('.cell--name a')?.getAttribute('href');
|
||||
// Navigate or open editor route
|
||||
if(id) window.location.href = id + '/edit';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "base.htm" %}
|
||||
|
||||
{% block title %}NanoShare{% 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="noindex, nofollow" />
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
: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}
|
||||
@media (prefers-color-scheme: light){
|
||||
:root{--bg:#f6f7fb;--panel:#fff;--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)}
|
||||
}
|
||||
*{box-sizing:border-box} html,body{height:100%}
|
||||
body{
|
||||
margin:0;color:var(--text);
|
||||
background: radial-gradient(900px 420px at 10% -10%, rgba(124,158,255,.16), transparent 60%),
|
||||
radial-gradient(700px 380px at 110% 10%, rgba(142,246,255,.14), transparent 60%),
|
||||
var(--bg);
|
||||
font:500 14px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,"Helvetica Neue",Arial
|
||||
}
|
||||
.wrap{max-width:980px;margin:clamp(10px,2.2vw,20px) auto;padding:clamp(12px,2.4vw,20px)}
|
||||
.card{background:linear-gradient(180deg,var(--panel),var(--panel-2));border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);padding:clamp(14px,2.6vw,22px)}
|
||||
.hero{display:grid;grid-template-columns:1.2fr .8fr;gap:18px;align-items:center}
|
||||
@media (max-width:900px){.hero{grid-template-columns:1fr}}
|
||||
.title{margin:0 0 6px;font-weight:900;letter-spacing:.2px;font-size:clamp(22px,2.6vw,30px);background:linear-gradient(90deg,var(--accent),var(--accent-2));-webkit-background-clip:text;background-clip:text;color:transparent}
|
||||
.subtitle{color:var(--muted);margin:0 0 12px;font-size:.98rem}
|
||||
.cta{display:flex;gap:10px;flex-wrap:wrap;margin-top:8px}
|
||||
.btn{--btn-br:color-mix(in srgb,var(--accent) 40%,var(--border));display:inline-flex;align-items:center;gap:.55ch;padding:.6rem .9rem;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:760;font-size:.95rem;text-decoration:none;cursor:pointer;transition:transform .06s,background .15s,border-color .15s,box-shadow .15s;box-shadow:0 2px 0 rgba(0,0,0,.08)}
|
||||
.btn:hover{background:color-mix(in srgb,var(--accent) 28%,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(.985)}
|
||||
.btn-ghost{background:transparent;border-color:var(--border)}
|
||||
.glass{min-height:180px;border-radius:calc(var(--radius) - 4px);border:1px dashed color-mix(in srgb,var(--accent) 40%,var(--border));background:radial-gradient(120% 120% at 100% 0%, color-mix(in srgb,var(--accent) 14%, transparent), transparent 40%),radial-gradient(120% 120% at 0% 100%, color-mix(in srgb,var(--accent-2) 14%, transparent), transparent 40%),color-mix(in srgb,var(--panel-2) 70%, transparent);display:flex;align-items:center;justify-content:center;text-align:center;padding:16px}
|
||||
.hero-badge{display:inline-flex;align-items:center;gap:.6ch;padding:.4rem .7rem;border-radius:999px;background:color-mix(in srgb,var(--accent) 16%,transparent);border:1px solid color-mix(in srgb,var(--accent) 32%,var(--border));color:var(--accent);font-weight:700;font-size:.9rem}
|
||||
.grid{margin-top:14px;display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
|
||||
@media (max-width:980px){.grid{grid-template-columns:repeat(2,1fr)}}
|
||||
@media (max-width:520px){.grid{grid-template-columns:1fr}}
|
||||
.feature{background:linear-gradient(180deg,color-mix(in srgb,var(--panel) 90%,transparent),color-mix(in srgb,var(--panel-2) 85%,transparent));border:1px solid var(--border);border-radius:12px;padding:12px;display:flex;gap:10px;align-items:flex-start;transition:transform .08s,background .2s,border-color .2s,box-shadow .2s}
|
||||
.feature:hover{transform:translateY(-1px);border-color:color-mix(in srgb,var(--accent) 35%,var(--border));box-shadow:0 10px 24px rgba(0,0,0,.14)}
|
||||
.ic{font-size:18px;filter:drop-shadow(0 4px 12px rgba(0,0,0,.2))}
|
||||
.fx-title{margin:0 0 2px;font-weight:800;font-size:.98rem}
|
||||
.fx-sub{margin:0;color:var(--muted);font-size:.9rem}
|
||||
.foot{margin-top:16px;color:var(--muted);font-size:.92rem;text-align:center}
|
||||
.foot a{color:var(--accent);text-decoration:none}
|
||||
.foot a:hover{text-decoration:underline}
|
||||
/* Auth widgets */
|
||||
.providers{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px}
|
||||
.prov{display:inline-flex;align-items:center;gap:.6ch;padding:.55rem .85rem;border-radius:12px;border:1px solid color-mix(in srgb,var(--accent) 40%,var(--border));background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 12%,transparent),transparent);text-decoration:none;color:var(--text);font-weight:760}
|
||||
.prov:hover{border-color:color-mix(in srgb,var(--accent) 60%,var(--border))}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main class="wrap">
|
||||
<section class="card hero" aria-label="Intro">
|
||||
<div>
|
||||
<h1 class="title">NanoShare</h1>
|
||||
<p class="subtitle">This instance is private. Please sign in to continue.</p>
|
||||
<div class="providers">
|
||||
<a class="prov" href="{{ url_for('auth_login.login') }}">🔐 Sign in with Authentik</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="glass">
|
||||
<span class="hero-badge">🔒 OAuth protected · ⚡ Lightweight · 🚀 Async</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid" aria-label="Highlights">
|
||||
<article class="feature"><div class="ic">🔗</div><div><h3 class="fx-title">Direct links</h3><p class="fx-sub">Clean URLs—no ads or gates.</p></div></article>
|
||||
<article class="feature"><div class="ic">🧰</div><div><h3 class="fx-title">Any file type</h3><p class="fx-sub">No re-encoding. Originals preserved.</p></div></article>
|
||||
<article class="feature"><div class="ic">⏱️</div><div><h3 class="fx-title">Expirations</h3><p class="fx-sub">Auto-expire links or set to never.</p></div></article>
|
||||
<article class="feature"><div class="ic">🛡️</div><div><h3 class="fx-title">Private by default</h3><p class="fx-sub">Access gated behind OAuth login.</p></div></article>
|
||||
</section>
|
||||
|
||||
<p class="foot">
|
||||
Built on Python Quart. View the <a href="https://git.yiprawr.dev/daniel156161/simple-picoshare" rel="nofollow">source repo</a>.
|
||||
</p>
|
||||
</main>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user