feat: add thumbnail metadata and download endpoint

- Extend `BoxFile` with thumbnail path/status fields and internal URL
- Populate `ThumbnailURL` when a thumbnail path is present during decoration
- Add `/box/:id/thumbnails/:file_id` route and handler to serve JPEG thumbnails
- Introduce thumbnail status constants to standardize processing state reportingfeat: add thumbnail metadata and download endpoint

- Extend `BoxFile` with thumbnail path/status fields and internal URL
- Populate `ThumbnailURL` when a thumbnail path is present during decoration
- Add `/box/:id/thumbnails/:file_id` route and handler to serve JPEG thumbnails
- Introduce thumbnail status constants to standardize processing state reporting
This commit is contained in:
2026-04-28 18:44:16 +03:00
parent c1489d1fbb
commit f1600faa8d
12 changed files with 400 additions and 17 deletions

View File

@@ -19,6 +19,14 @@ let selectedFiles = [];
let statusTimer = null;
let shareURL = "";
function revokePreviewURLs() {
selectedFiles.forEach((selectedFile) => {
if (selectedFile.previewURL) {
URL.revokeObjectURL(selectedFile.previewURL);
}
});
}
function formatBytes(bytes) {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
@@ -167,10 +175,11 @@ function setRowProgress(row, percent) {
function createFileRow(selectedFile) {
const row = document.createElement("div");
row.className = "upload-file-row";
row.classList.toggle("has-thumbnail", Boolean(selectedFile.previewURL));
const icon = document.createElement("img");
icon.className = "upload-file-icon";
icon.src = iconForFile(selectedFile.file);
icon.src = selectedFile.previewURL || iconForFile(selectedFile.file);
icon.alt = "";
icon.setAttribute("aria-hidden", "true");
@@ -197,8 +206,10 @@ function createFileRow(selectedFile) {
}
function updateSelectedFiles(files) {
revokePreviewURLs();
selectedFiles = Array.from(files || []).map((file) => ({
file,
previewURL: file.type.startsWith("image/") ? URL.createObjectURL(file) : "",
loaded: 0,
row: null,
uploaded: false,
@@ -438,7 +449,10 @@ if (uploadForm) {
selectedFile.boxID = box.box_id;
selectedFile.boxFile = box.files[index];
const icon = selectedFile.row.querySelector(".upload-file-icon");
if (icon && selectedFile.boxFile.icon_path) {
if (icon && selectedFile.boxFile.thumbnail_path) {
selectedFile.row.classList.add("has-thumbnail");
icon.src = selectedFile.boxFile.thumbnail_path;
} else if (icon && selectedFile.boxFile.icon_path && !selectedFile.previewURL) {
icon.src = selectedFile.boxFile.icon_path;
}
});
@@ -489,3 +503,5 @@ if (shareButton) {
}
});
}
window.addEventListener("beforeunload", revokePreviewURLs);

View File

@@ -16,12 +16,14 @@ function updateBoxFile(file) {
}
const meta = item.querySelector(".box-file-meta");
const icon = item.querySelector(".box-file-icon");
const isComplete = file.status === "complete";
const isFailed = file.status === "failed";
item.classList.toggle("is-complete", isComplete);
item.classList.toggle("is-failed", isFailed);
item.classList.toggle("is-loading", !isComplete && !isFailed);
item.classList.toggle("has-thumbnail", Boolean(file.thumbnail_path));
item.dataset.status = file.status;
item.title = file.title;
@@ -38,6 +40,10 @@ function updateBoxFile(file) {
if (meta) {
meta.textContent = `${file.status_label} · ${file.size_label}`;
}
if (icon) {
icon.src = file.thumbnail_path || file.icon_path;
}
}
async function refreshBoxStatus() {
@@ -59,7 +65,11 @@ async function refreshBoxStatus() {
boxStatus.textContent = `${completeCount}/${result.files.length} ready`;
}
return result.files.some((file) => file.status === "pending" || file.status === "uploading");
return result.files.some((file) => {
const isUploading = file.status === "pending" || file.status === "uploading";
const isWaitingForThumbnail = file.status === "complete" && !file.thumbnail_status && !file.thumbnail_path;
return isUploading || isWaitingForThumbnail || file.thumbnail_status === "processing";
});
}
if (boxPanel) {