feat: generate client-side thumbnails
This commit is contained in:
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user