286 lines
9.1 KiB
HTML
286 lines
9.1 KiB
HTML
{% 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 }}" class="local-time"></time></td>
|
||
<td><time datetime="{{ file.expires_at }}" class="local-time"></time></td>
|
||
<td class="cell--right">
|
||
<div class="actions">
|
||
<button class="icon-btn" title="Info">
|
||
<a href="{{ url_for('side_main.file_info', file_id=file.file_id) }}">ℹ️ <span class="sr-only">Info</span></a>
|
||
</button>
|
||
<button class="icon-btn" title="Edit">
|
||
<a href="{{ url_for('side_main.file_edit', file_id=file.file_id) }}">✏️ <span class="sr-only">Edit</span></a>
|
||
</button>
|
||
<button class="icon-btn" title="Copy link" data-action="copy">📋 <span class="sr-only">Copy</span></button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
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.');
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll("time.local-time").forEach(timeEl => {
|
||
const datetime = timeEl.getAttribute("datetime");
|
||
if (!datetime) return;
|
||
|
||
const date = new Date(datetime);
|
||
|
||
timeEl.title = date.toISOString();
|
||
timeEl.textContent = date.toLocaleString(undefined, {
|
||
year: "numeric",
|
||
month: "short",
|
||
day: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: undefined,
|
||
hour12: false,
|
||
});
|
||
});
|
||
</script>
|
||
{% endblock %}
|