feat: generate client-side thumbnails

This commit is contained in:
2026-06-13 07:23:33 +02:00
parent ffd2ca887d
commit 7193457f36

View File

@@ -1123,6 +1123,94 @@ function htmlPage(): string {
})[character]);
}
function buildRemoteImageUrl(shareUrlValue, entryHref) {
const params = new URLSearchParams({
share: shareUrlValue.trim(),
url: entryHref
});
return "/api/nextcloud/blob?" + params.toString();
}
async function loadImageElement(blob) {
const objectUrl = URL.createObjectURL(blob);
const image = new Image();
try {
await new Promise((resolve, reject) => {
image.onload = () => resolve();
image.onerror = () => reject(new Error("Could not decode image."));
image.src = objectUrl;
});
return image;
} finally {
URL.revokeObjectURL(objectUrl);
}
}
async function createThumbnailObjectUrl(blob) {
const maxDimension = 320;
const quality = 0.82;
let width = 0;
let height = 0;
let drawSource = null;
let closeBitmap = null;
let sourceImage = null;
if (typeof createImageBitmap === "function") {
const bitmap = await createImageBitmap(blob);
width = bitmap.width;
height = bitmap.height;
drawSource = bitmap;
closeBitmap = () => bitmap.close();
} else {
sourceImage = await loadImageElement(blob);
width = sourceImage.naturalWidth;
height = sourceImage.naturalHeight;
drawSource = sourceImage;
}
const scale = Math.min(1, maxDimension / Math.max(width, height));
const targetWidth = Math.max(1, Math.round(width * scale));
const targetHeight = Math.max(1, Math.round(height * scale));
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) {
if (closeBitmap) {
closeBitmap();
}
throw new Error("Canvas 2D context unavailable.");
}
canvas.width = targetWidth;
canvas.height = targetHeight;
context.drawImage(drawSource, 0, 0, targetWidth, targetHeight);
const thumbnailBlob = await new Promise((resolve, reject) => {
canvas.toBlob(
(result) => {
if (result) {
resolve(result);
return;
}
reject(new Error("Could not create thumbnail."));
},
"image/jpeg",
quality
);
});
if (closeBitmap) {
closeBitmap();
}
if (sourceImage) {
sourceImage.removeAttribute("src");
}
return URL.createObjectURL(thumbnailBlob);
}
function setTheme(theme) {
const resolvedTheme = theme === "light" ? "light" : "dark";
document.body.dataset.theme = resolvedTheme;
@@ -1452,6 +1540,7 @@ function htmlPage(): string {
function clearGallery() {
cancelScheduledRender();
closeOverlayView();
state.photos = [];
state.visiblePhotos = [];
state.activePhotoId = null;
@@ -1501,6 +1590,7 @@ function htmlPage(): string {
function closeOverlayView() {
overlay.classList.remove("open");
overlay.setAttribute("aria-hidden", "true");
overlayImage.removeAttribute("src");
}
closeOverlay.addEventListener("click", closeOverlayView);
@@ -1950,11 +2040,8 @@ function htmlPage(): string {
}
async function readRemoteImage(entry, shareUrlValue, signal) {
const params = new URLSearchParams({
share: shareUrlValue.trim(),
url: entry.href
});
const response = await fetch("/api/nextcloud/blob?" + params.toString(), {
const fullUrl = buildRemoteImageUrl(shareUrlValue, entry.href);
const response = await fetch(fullUrl, {
signal
});
if (!response.ok) {
@@ -1966,8 +2053,8 @@ function htmlPage(): string {
const capturedAt =
normalizeExifDate(tags?.DateTimeOriginal?.value ?? tags?.DateTimeOriginal) ||
parseDateOrNull(entry.lastModified);
const objectUrl = URL.createObjectURL(blob);
state.objectUrls.push(objectUrl);
const thumbUrl = await createThumbnailObjectUrl(blob);
state.objectUrls.push(thumbUrl);
return {
id: entry.href,
@@ -1975,8 +2062,8 @@ function htmlPage(): string {
latitude: gps?.latitude ?? null,
longitude: gps?.longitude ?? null,
capturedAt,
thumbUrl: objectUrl,
fullUrl: objectUrl,
thumbUrl,
fullUrl,
source: "nextcloud"
};
}