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]); })[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) { function setTheme(theme) {
const resolvedTheme = theme === "light" ? "light" : "dark"; const resolvedTheme = theme === "light" ? "light" : "dark";
document.body.dataset.theme = resolvedTheme; document.body.dataset.theme = resolvedTheme;
@@ -1452,6 +1540,7 @@ function htmlPage(): string {
function clearGallery() { function clearGallery() {
cancelScheduledRender(); cancelScheduledRender();
closeOverlayView();
state.photos = []; state.photos = [];
state.visiblePhotos = []; state.visiblePhotos = [];
state.activePhotoId = null; state.activePhotoId = null;
@@ -1501,6 +1590,7 @@ function htmlPage(): string {
function closeOverlayView() { function closeOverlayView() {
overlay.classList.remove("open"); overlay.classList.remove("open");
overlay.setAttribute("aria-hidden", "true"); overlay.setAttribute("aria-hidden", "true");
overlayImage.removeAttribute("src");
} }
closeOverlay.addEventListener("click", closeOverlayView); closeOverlay.addEventListener("click", closeOverlayView);
@@ -1950,11 +2040,8 @@ function htmlPage(): string {
} }
async function readRemoteImage(entry, shareUrlValue, signal) { async function readRemoteImage(entry, shareUrlValue, signal) {
const params = new URLSearchParams({ const fullUrl = buildRemoteImageUrl(shareUrlValue, entry.href);
share: shareUrlValue.trim(), const response = await fetch(fullUrl, {
url: entry.href
});
const response = await fetch("/api/nextcloud/blob?" + params.toString(), {
signal signal
}); });
if (!response.ok) { if (!response.ok) {
@@ -1966,8 +2053,8 @@ function htmlPage(): string {
const capturedAt = const capturedAt =
normalizeExifDate(tags?.DateTimeOriginal?.value ?? tags?.DateTimeOriginal) || normalizeExifDate(tags?.DateTimeOriginal?.value ?? tags?.DateTimeOriginal) ||
parseDateOrNull(entry.lastModified); parseDateOrNull(entry.lastModified);
const objectUrl = URL.createObjectURL(blob); const thumbUrl = await createThumbnailObjectUrl(blob);
state.objectUrls.push(objectUrl); state.objectUrls.push(thumbUrl);
return { return {
id: entry.href, id: entry.href,
@@ -1975,8 +2062,8 @@ function htmlPage(): string {
latitude: gps?.latitude ?? null, latitude: gps?.latitude ?? null,
longitude: gps?.longitude ?? null, longitude: gps?.longitude ?? null,
capturedAt, capturedAt,
thumbUrl: objectUrl, thumbUrl,
fullUrl: objectUrl, fullUrl,
source: "nextcloud" source: "nextcloud"
}; };
} }