UPDATED
This commit is contained in:
+98
-73
@@ -6,7 +6,11 @@ const STORAGE_KEY = "filehost.uploads";
|
||||
|
||||
const dropzone = $("dropzone");
|
||||
const fileInput = $("fileInput");
|
||||
const chosenFile = $("chosenFile");
|
||||
const pwInput = $("pwInput");
|
||||
const uploadBtn = $("uploadBtn");
|
||||
const newUploadBtn = $("newUploadBtn");
|
||||
const clearListBtn = $("clearListBtn");
|
||||
const progress = $("progress");
|
||||
const progressBar = $("progressBar");
|
||||
const progressPct = $("progressPct");
|
||||
@@ -14,20 +18,17 @@ const progressName = $("progressName");
|
||||
const result = $("result");
|
||||
const resultUrl = $("resultUrl");
|
||||
const resultName = $("resultName");
|
||||
const resultSize = $("resultSize");
|
||||
const resultLock = $("resultLock");
|
||||
const copyBtn = $("copyBtn");
|
||||
const deleteBtn = $("deleteBtn");
|
||||
const againBtn = $("againBtn");
|
||||
const notice = $("notice");
|
||||
const fileListWrap = $("fileListWrap");
|
||||
const fileList = $("fileList");
|
||||
const clearListBtn = $("clearListBtn");
|
||||
const fileCount = $("fileCount");
|
||||
const emptyState = $("emptyState");
|
||||
|
||||
let current = null;
|
||||
let currentFile = null;
|
||||
let uploading = false;
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) return "-";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
let value = bytes;
|
||||
@@ -57,18 +58,21 @@ function setProgress(loaded, total) {
|
||||
|
||||
function reset() {
|
||||
uploading = false;
|
||||
current = null;
|
||||
currentFile = null;
|
||||
result.classList.add("hidden");
|
||||
resultLock.classList.add("hidden");
|
||||
progress.classList.add("hidden");
|
||||
progressBar.style.width = "0%";
|
||||
progressPct.textContent = "0%";
|
||||
chosenFile.classList.add("hidden");
|
||||
chosenFile.textContent = "";
|
||||
fileInput.value = "";
|
||||
pwInput.value = "";
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
clearNotice();
|
||||
}
|
||||
|
||||
// ---- client-side upload history (localStorage) ----
|
||||
// ---- client-side history (localStorage) ----
|
||||
|
||||
function loadUploads() {
|
||||
try {
|
||||
@@ -84,7 +88,7 @@ function saveUploads(list) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||||
} catch {
|
||||
/* storage unavailable (e.g. private mode) — ignore */
|
||||
/* storage unavailable — ignore */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,80 +146,79 @@ function setCopyFeedback(btn, label) {
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
function formatDate(iso) {
|
||||
try {
|
||||
return new Date(iso).toLocaleString();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const list = loadUploads();
|
||||
if (list.length === 0) {
|
||||
fileListWrap.classList.add("hidden");
|
||||
fileList.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
fileListWrap.classList.remove("hidden");
|
||||
fileList.innerHTML = "";
|
||||
emptyState.classList.toggle("hidden", list.length > 0);
|
||||
fileCount.textContent = list.length === 0 ? "" : `${list.length} file${list.length === 1 ? "" : "s"}`;
|
||||
|
||||
for (const item of list) {
|
||||
const li = document.createElement("li");
|
||||
li.className = "file-item";
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "file-item-meta";
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const nameTd = document.createElement("td");
|
||||
const nameWrap = document.createElement("div");
|
||||
nameWrap.className = "file-name";
|
||||
const name = document.createElement("span");
|
||||
name.className = "file-item-name";
|
||||
name.textContent = item.originalName;
|
||||
name.title = item.originalName;
|
||||
const lock = document.createElement("span");
|
||||
lock.className = "lock";
|
||||
lock.textContent = "Protected";
|
||||
nameWrap.appendChild(name);
|
||||
nameWrap.appendChild(lock);
|
||||
nameTd.appendChild(nameWrap);
|
||||
|
||||
if (item.protected) {
|
||||
const lock = document.createElement("span");
|
||||
lock.className = "lock";
|
||||
lock.textContent = "Password protected";
|
||||
name.appendChild(lock);
|
||||
}
|
||||
const sizeTd = document.createElement("td");
|
||||
sizeTd.className = "file-size";
|
||||
sizeTd.textContent = formatBytes(item.size);
|
||||
|
||||
const sub = document.createElement("span");
|
||||
sub.className = "file-item-sub";
|
||||
let subText = formatBytes(item.size || 0);
|
||||
if (item.createdAt) {
|
||||
try {
|
||||
subText += ` \u00b7 ${new Date(item.createdAt).toLocaleString()}`;
|
||||
} catch {
|
||||
/* ignore date formatting */
|
||||
}
|
||||
}
|
||||
sub.textContent = subText;
|
||||
|
||||
meta.appendChild(name);
|
||||
meta.appendChild(sub);
|
||||
const dateTd = document.createElement("td");
|
||||
dateTd.className = "file-date";
|
||||
dateTd.textContent = formatDate(item.createdAt);
|
||||
|
||||
const actionsTd = document.createElement("td");
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "file-item-actions";
|
||||
actions.className = "row-actions";
|
||||
|
||||
const dl = document.createElement("a");
|
||||
dl.className = "btn-link";
|
||||
dl.className = "btn btn-primary";
|
||||
dl.href = downloadUrl(item);
|
||||
dl.textContent = "Download";
|
||||
dl.title = "Open download link";
|
||||
|
||||
const copy = document.createElement("button");
|
||||
copy.type = "button";
|
||||
copy.className = "btn btn-ghost";
|
||||
copy.textContent = "Copy";
|
||||
copy.addEventListener("click", async () => {
|
||||
const ok = await copyText(downloadUrl(item));
|
||||
setCopyFeedback(copy, ok ? "Copied" : "Copy failed");
|
||||
setCopyFeedback(copy, ok ? "Copied" : "Failed");
|
||||
});
|
||||
|
||||
const del = document.createElement("button");
|
||||
del.type = "button";
|
||||
del.className = "danger";
|
||||
del.className = "btn btn-danger";
|
||||
del.textContent = "Delete";
|
||||
del.addEventListener("click", () => deleteFile(item, () => removeUpload(item.id)));
|
||||
|
||||
actions.appendChild(dl);
|
||||
actions.appendChild(copy);
|
||||
actions.appendChild(del);
|
||||
actionsTd.appendChild(actions);
|
||||
|
||||
li.appendChild(meta);
|
||||
li.appendChild(actions);
|
||||
fileList.appendChild(li);
|
||||
tr.appendChild(nameTd);
|
||||
tr.appendChild(sizeTd);
|
||||
tr.appendChild(dateTd);
|
||||
tr.appendChild(actionsTd);
|
||||
fileList.appendChild(tr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,32 +254,30 @@ function showResult(data) {
|
||||
progress.classList.add("hidden");
|
||||
result.classList.remove("hidden");
|
||||
resultName.textContent = data.originalName;
|
||||
resultSize.textContent = formatBytes(data.size);
|
||||
resultUrl.value = downloadUrl(data);
|
||||
resultLock.classList.toggle("hidden", !data.protected);
|
||||
current = data;
|
||||
addUpload({
|
||||
id: data.id,
|
||||
key: data.key,
|
||||
url: data.url,
|
||||
originalName: data.originalName,
|
||||
size: data.size,
|
||||
protected: !!data.protected,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
function startUpload(file) {
|
||||
function startUpload(file, password) {
|
||||
if (uploading) return;
|
||||
clearNotice();
|
||||
progress.classList.remove("hidden");
|
||||
progressName.textContent = file.name;
|
||||
setProgress(0, file.size);
|
||||
uploading = true;
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.textContent = "Uploading\u2026";
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
const form = new FormData();
|
||||
if (pwInput.value) form.append("password", pwInput.value);
|
||||
form.append("password", password);
|
||||
form.append("file", file);
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
@@ -285,11 +286,13 @@ function startUpload(file) {
|
||||
|
||||
xhr.addEventListener("load", () => {
|
||||
uploading = false;
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
progress.classList.add("hidden");
|
||||
try {
|
||||
showResult(JSON.parse(xhr.responseText));
|
||||
} catch {
|
||||
progress.classList.add("hidden");
|
||||
showNotice("Invalid server response.", "error");
|
||||
}
|
||||
return;
|
||||
@@ -307,12 +310,16 @@ function startUpload(file) {
|
||||
|
||||
xhr.addEventListener("error", () => {
|
||||
uploading = false;
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
progress.classList.add("hidden");
|
||||
showNotice("Network error during upload.", "error");
|
||||
});
|
||||
|
||||
xhr.addEventListener("abort", () => {
|
||||
uploading = false;
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.textContent = "Upload";
|
||||
progress.classList.add("hidden");
|
||||
});
|
||||
|
||||
@@ -320,13 +327,30 @@ function startUpload(file) {
|
||||
xhr.send(form);
|
||||
}
|
||||
|
||||
function handleFile(file) {
|
||||
function selectFile(file) {
|
||||
if (!file) return;
|
||||
if (file.size === 0) {
|
||||
showNotice("Empty files are not supported.", "error");
|
||||
return;
|
||||
}
|
||||
startUpload(file);
|
||||
currentFile = file;
|
||||
chosenFile.textContent = file.name;
|
||||
chosenFile.classList.remove("hidden");
|
||||
clearNotice();
|
||||
}
|
||||
|
||||
function submitUpload() {
|
||||
if (!currentFile) {
|
||||
showNotice("Choose a file first.", "error");
|
||||
return;
|
||||
}
|
||||
const password = pwInput.value;
|
||||
if (!password) {
|
||||
showNotice("A password is required to protect this file.", "error");
|
||||
pwInput.focus();
|
||||
return;
|
||||
}
|
||||
startUpload(currentFile, password);
|
||||
}
|
||||
|
||||
dropzone.addEventListener("click", () => {
|
||||
@@ -342,7 +366,18 @@ dropzone.addEventListener("keydown", (e) => {
|
||||
|
||||
fileInput.addEventListener("change", () => {
|
||||
const file = fileInput.files ? fileInput.files[0] : null;
|
||||
handleFile(file);
|
||||
selectFile(file);
|
||||
});
|
||||
|
||||
pwInput.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") submitUpload();
|
||||
});
|
||||
|
||||
uploadBtn.addEventListener("click", submitUpload);
|
||||
|
||||
newUploadBtn.addEventListener("click", () => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
pwInput.focus();
|
||||
});
|
||||
|
||||
let dragDepth = 0;
|
||||
@@ -365,28 +400,18 @@ document.addEventListener("drop", (e) => {
|
||||
dropzone.classList.remove("drag");
|
||||
if (uploading) return;
|
||||
const file = e.dataTransfer && e.dataTransfer.files ? e.dataTransfer.files[0] : null;
|
||||
if (file) handleFile(file);
|
||||
selectFile(file);
|
||||
});
|
||||
|
||||
copyBtn.addEventListener("click", async () => {
|
||||
const ok = await copyText(resultUrl.value);
|
||||
setCopyFeedback(copyBtn, ok ? "Copied" : "Copy failed");
|
||||
setCopyFeedback(copyBtn, ok ? "Copied" : "Failed");
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener("click", () => {
|
||||
if (!current) return;
|
||||
deleteFile(current, () => {
|
||||
removeUpload(current.id);
|
||||
reset();
|
||||
});
|
||||
});
|
||||
|
||||
againBtn.addEventListener("click", reset);
|
||||
|
||||
clearListBtn.addEventListener("click", () => {
|
||||
if (loadUploads().length === 0) return;
|
||||
const ok = window.confirm(
|
||||
"Clear this list? This only removes the entries from THIS browser. The uploaded files remain on the server.",
|
||||
"Clear this list? This only removes the entries from THIS browser. The files remain on the server.",
|
||||
);
|
||||
if (!ok) return;
|
||||
saveUploads([]);
|
||||
|
||||
+86
-56
@@ -5,69 +5,99 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>File drop</title>
|
||||
<title>File Drop</title>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="card">
|
||||
<h1>File drop</h1>
|
||||
<p class="tagline">Drop a file to get a private download link. Files are kept forever.</p>
|
||||
|
||||
<div id="dropzone" class="dropzone" role="button" tabindex="0" aria-label="Upload a file">
|
||||
<input type="file" id="fileInput" hidden />
|
||||
<p class="dz-main">Drag & drop a file here</p>
|
||||
<p class="dz-sub">or click to choose</p>
|
||||
</div>
|
||||
|
||||
<div class="pw-row">
|
||||
<input
|
||||
type="password"
|
||||
id="pwInput"
|
||||
placeholder="Optional password (protects the download)"
|
||||
autocomplete="off"
|
||||
aria-label="Optional download password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="progress" class="progress hidden">
|
||||
<div class="progress-label">
|
||||
<span id="progressName"></span>
|
||||
<span id="progressPct">0%</span>
|
||||
<div class="app">
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<div class="logo" aria-hidden="true">FD</div>
|
||||
<div class="brand-text">
|
||||
<h1>File Drop</h1>
|
||||
<span class="sub">Private, password-protected file hosting</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div id="progressBar" class="progress-bar"></div>
|
||||
<div class="topbar-actions">
|
||||
<button id="newUploadBtn" type="button" class="btn btn-primary">New upload</button>
|
||||
<button id="clearListBtn" type="button" class="btn btn-ghost">Clear list</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="result" class="result hidden">
|
||||
<div class="result-meta">
|
||||
<strong id="resultName"></strong>
|
||||
<span id="resultSize"></span>
|
||||
<span id="resultLock" class="lock hidden">Password protected</span>
|
||||
</div>
|
||||
<div class="url-row">
|
||||
<input id="resultUrl" type="text" readonly aria-label="Download URL" />
|
||||
<button id="copyBtn" type="button">Copy</button>
|
||||
</div>
|
||||
<div class="result-actions">
|
||||
<button id="deleteBtn" type="button" class="danger">Delete file</button>
|
||||
<button id="againBtn" type="button" class="ghost">Upload another</button>
|
||||
</div>
|
||||
</div>
|
||||
<main class="layout">
|
||||
<aside class="panel upload-panel">
|
||||
<h2>Upload a file</h2>
|
||||
|
||||
<div id="notice" class="hidden"></div>
|
||||
<div id="dropzone" class="dropzone" role="button" tabindex="0" aria-label="Choose a file">
|
||||
<input type="file" id="fileInput" hidden />
|
||||
<p class="dz-main">Drag & drop a file here</p>
|
||||
<p class="dz-sub">or click to choose</p>
|
||||
<p id="chosenFile" class="dz-file hidden"></p>
|
||||
</div>
|
||||
|
||||
<section id="fileListWrap" class="files hidden" aria-label="Your uploaded files">
|
||||
<div class="files-head">
|
||||
<h2>Your files</h2>
|
||||
<button id="clearListBtn" type="button" class="ghost">Clear list</button>
|
||||
</div>
|
||||
<p class="files-note">
|
||||
Stored locally in this browser only. Other visitors cannot see them.
|
||||
</p>
|
||||
<ul id="fileList"></ul>
|
||||
</section>
|
||||
</main>
|
||||
<div class="field">
|
||||
<label for="pwInput">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="pwInput"
|
||||
placeholder="Required — protects the download"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="progress" class="progress hidden">
|
||||
<div class="progress-label">
|
||||
<span id="progressName"></span>
|
||||
<span id="progressPct">0%</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div id="progressBar" class="progress-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="uploadBtn" type="button" class="btn btn-primary btn-block">Upload</button>
|
||||
|
||||
<div id="result" class="result hidden">
|
||||
<div class="result-head">
|
||||
<span id="resultName"></span>
|
||||
<span class="lock">Protected</span>
|
||||
</div>
|
||||
<div class="url-row">
|
||||
<input id="resultUrl" type="text" readonly aria-label="Download URL" />
|
||||
<button id="copyBtn" type="button" class="btn btn-primary">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="notice" class="hidden"></div>
|
||||
</aside>
|
||||
|
||||
<section class="panel files-panel">
|
||||
<div class="panel-head">
|
||||
<h2>Your files</h2>
|
||||
<span id="fileCount" class="count"></span>
|
||||
</div>
|
||||
<p class="files-note">
|
||||
Stored locally in this browser only. Every file requires its password.
|
||||
</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table class="files-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-name">Name</th>
|
||||
<th class="col-size">Size</th>
|
||||
<th class="col-date">Uploaded</th>
|
||||
<th class="col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fileList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<p id="emptyState" class="empty">No files yet. Upload your first file.</p>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
+335
-208
@@ -1,13 +1,15 @@
|
||||
:root {
|
||||
--bg: #0f1115;
|
||||
--card: #171a21;
|
||||
--border: #2a2f3a;
|
||||
--text: #e6e8ee;
|
||||
--muted: #9aa3b2;
|
||||
--accent: #4f8cff;
|
||||
--accent-hover: #3a74e0;
|
||||
--danger: #e05252;
|
||||
--bg: #0e1013;
|
||||
--surface: #15181d;
|
||||
--surface-2: #1a1e24;
|
||||
--border: #262b33;
|
||||
--text: #e7eaf0;
|
||||
--muted: #98a1af;
|
||||
--accent: #3f7bf6;
|
||||
--accent-strong: #2f63d8;
|
||||
--danger: #e05555;
|
||||
--ok: #46b076;
|
||||
--radius: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -20,37 +22,151 @@ body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px 48px;
|
||||
}
|
||||
|
||||
/* ---- top bar ---- */
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 20px 0 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 28px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 24px;
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-strong));
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
margin: 0 0 20px;
|
||||
.brand-text h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ---- buttons ---- */
|
||||
.btn {
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius);
|
||||
padding: 9px 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-strong);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
border-color: var(--border);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--muted);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: transparent;
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: rgba(224, 85, 85, 0.1);
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* ---- layout ---- */
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 340px 1fr;
|
||||
gap: 20px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
/* ---- upload panel ---- */
|
||||
.upload-panel {
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 40px 20px;
|
||||
border: 1.5px dashed var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 26px 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
@@ -61,61 +177,67 @@ h1 {
|
||||
.dropzone.drag,
|
||||
.dropzone:focus-visible {
|
||||
border-color: var(--accent);
|
||||
background: rgba(79, 140, 255, 0.06);
|
||||
background: rgba(63, 123, 246, 0.06);
|
||||
}
|
||||
|
||||
.dz-main {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dz-sub {
|
||||
margin: 6px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dz-file {
|
||||
margin: 12px 0 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--accent);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pw-row {
|
||||
margin-top: 12px;
|
||||
.field {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.pw-row input {
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.field input {
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
border-radius: var(--radius);
|
||||
padding: 11px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pw-row input:focus {
|
||||
.field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.lock {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
background: rgba(79, 140, 255, 0.12);
|
||||
border: 1px solid rgba(79, 140, 255, 0.3);
|
||||
border-radius: 99px;
|
||||
padding: 2px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* ---- progress ---- */
|
||||
.progress {
|
||||
margin-top: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -127,8 +249,8 @@ h1 {
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
height: 6px;
|
||||
background: var(--surface-2);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -141,20 +263,27 @@ h1 {
|
||||
transition: width 0.15s;
|
||||
}
|
||||
|
||||
/* ---- result banner ---- */
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface-2);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
font-size: 14px;
|
||||
.result-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.result-meta strong {
|
||||
color: var(--text);
|
||||
.result-head span:first-child {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.url-row {
|
||||
@@ -168,65 +297,159 @@ h1 {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
/* ---- files panel ---- */
|
||||
.panel-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 9px 14px;
|
||||
font-size: 13px;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
transition: background 0.15s;
|
||||
.panel-head h2 {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--accent-hover);
|
||||
.count {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
background: transparent;
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
button.danger:hover {
|
||||
background: rgba(224, 82, 82, 0.1);
|
||||
}
|
||||
|
||||
button.ghost {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
.files-note {
|
||||
margin: 0 0 16px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
button.ghost:hover {
|
||||
color: var(--text);
|
||||
border-color: var(--muted);
|
||||
.table-wrap {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
#notice {
|
||||
margin-top: 16px;
|
||||
.files-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 520px;
|
||||
}
|
||||
|
||||
.files-table th,
|
||||
.files-table td {
|
||||
text-align: left;
|
||||
padding: 12px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.files-table thead th {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.6px;
|
||||
color: var(--muted);
|
||||
background: var(--surface-2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.files-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.files-table tbody tr:hover td {
|
||||
background: rgba(255, 255, 255, 0.015);
|
||||
}
|
||||
|
||||
.col-name {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.col-size {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.col-date {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
width: 28%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.lock {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--muted);
|
||||
background: rgba(63, 123, 246, 0.12);
|
||||
border: 1px solid rgba(63, 123, 246, 0.3);
|
||||
border-radius: 99px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.file-sub {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.file-size,
|
||||
.file-date {
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.row-actions .btn {
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
padding: 28px 0 6px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---- notice ---- */
|
||||
#notice {
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
#notice.error {
|
||||
color: var(--danger);
|
||||
background: rgba(224, 82, 82, 0.08);
|
||||
border: 1px solid rgba(224, 82, 82, 0.3);
|
||||
background: rgba(224, 85, 85, 0.08);
|
||||
border: 1px solid rgba(224, 85, 85, 0.3);
|
||||
}
|
||||
|
||||
#notice.info {
|
||||
@@ -239,119 +462,23 @@ button.ghost:hover {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Client-side upload list */
|
||||
.files {
|
||||
margin-top: 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.files-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.files-head h2 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.files-note {
|
||||
margin: 6px 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#fileList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.file-item-meta {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.file-item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-item-sub {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
background: var(--accent);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.file-item-actions button {
|
||||
font-size: 12px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.card {
|
||||
padding: 20px;
|
||||
/* ---- responsive ---- */
|
||||
@media (max-width: 860px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
padding: 28px 16px;
|
||||
}
|
||||
|
||||
.url-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.file-item-actions {
|
||||
justify-content: flex-end;
|
||||
.upload-panel {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.app {
|
||||
padding: 0 12px 32px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export async function handleDownload(
|
||||
if (!meta) return notFound();
|
||||
if (!secretMatches(meta.downloadKeyHash, rawKey)) return notFound();
|
||||
|
||||
if (meta.passwordHash && !hasValidAuth(req, secret, rawKey)) {
|
||||
if (!hasValidAuth(req, secret, rawKey)) {
|
||||
const url = new URL(req.url);
|
||||
return renderPasswordForm(rawKey, meta.originalName, url.searchParams.has("e"));
|
||||
}
|
||||
|
||||
@@ -63,24 +63,21 @@ export async function handleUpload(
|
||||
}
|
||||
|
||||
if (!hasFile) return jsonError(400, "no file part in the upload");
|
||||
if (!password.trim()) return jsonError(400, "a password is required");
|
||||
|
||||
const originalName = sanitizeOriginalName(filename);
|
||||
const protected_ = password.trim().length > 0;
|
||||
await storage.writeMetadata({
|
||||
id,
|
||||
originalName,
|
||||
size,
|
||||
mimeType: mime,
|
||||
downloadKeyHash: hashSecret(key),
|
||||
passwordHash: protected_ ? hashPassword(password) : undefined,
|
||||
passwordHash: hashPassword(password),
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
log(
|
||||
"info",
|
||||
`upload ok id=${id} size=${size} name=${JSON.stringify(originalName)} protected=${protected_}`,
|
||||
);
|
||||
return json({ id, key, url: `/d/${key}`, originalName, size, protected: protected_ });
|
||||
log("info", `upload ok id=${id} size=${size} name=${JSON.stringify(originalName)}`);
|
||||
return json({ id, key, url: `/d/${key}`, originalName, size, protected: true });
|
||||
} catch (err) {
|
||||
await storage.delete(id);
|
||||
await storage.deletePartial(id);
|
||||
|
||||
Reference in New Issue
Block a user