diff --git a/src/server/request-handler.ts b/src/server/request-handler.ts index 394e266..3048760 100644 --- a/src/server/request-handler.ts +++ b/src/server/request-handler.ts @@ -993,6 +993,11 @@ function htmlPage(): string { } }; let activeImportController = null; + const IMPORT_RENDER_INTERVAL_MS = 180; + let scheduledRenderFrame = null; + let scheduledRenderTimer = null; + let scheduledRenderOptions = { fitMap: false }; + let lastRenderAt = 0; const map = L.map("map", { zoomControl: true }).setView([52.5208, 13.4095], 13); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { @@ -1376,6 +1381,7 @@ function htmlPage(): string { } function clearGallery() { + cancelScheduledRender(); state.photos = []; state.visiblePhotos = []; state.activePhotoId = null; @@ -1667,6 +1673,60 @@ function htmlPage(): string { } } + function runScheduledRender() { + scheduledRenderFrame = null; + const options = scheduledRenderOptions; + scheduledRenderOptions = { fitMap: false }; + lastRenderAt = performance.now(); + renderVisiblePhotos(options); + } + + function scheduleVisiblePhotoRender(options = {}) { + scheduledRenderOptions = { + fitMap: Boolean(scheduledRenderOptions.fitMap || options.fitMap) + }; + + if (scheduledRenderFrame !== null || scheduledRenderTimer !== null) { + return; + } + + const elapsed = performance.now() - lastRenderAt; + const delay = Math.max(0, IMPORT_RENDER_INTERVAL_MS - elapsed); + const queueFrame = () => { + scheduledRenderTimer = null; + scheduledRenderFrame = requestAnimationFrame(runScheduledRender); + }; + + if (delay === 0) { + queueFrame(); + } else { + scheduledRenderTimer = setTimeout(queueFrame, delay); + } + } + + function cancelScheduledRender() { + if (scheduledRenderFrame !== null) { + cancelAnimationFrame(scheduledRenderFrame); + scheduledRenderFrame = null; + } + + if (scheduledRenderTimer !== null) { + clearTimeout(scheduledRenderTimer); + scheduledRenderTimer = null; + } + + scheduledRenderOptions = { fitMap: false }; + } + + function flushVisiblePhotoRender(options = {}) { + const renderOptions = { + fitMap: Boolean(scheduledRenderOptions.fitMap || options.fitMap) + }; + cancelScheduledRender(); + lastRenderAt = performance.now(); + renderVisiblePhotos(renderOptions); + } + let timelinePointerState = null; let timelineClickSuppressed = false; @@ -1792,7 +1852,7 @@ function htmlPage(): string { function appendPhoto(photo) { state.photos.push(photo); - renderVisiblePhotos({ fitMap: state.photos.length === 1 }); + scheduleVisiblePhotoRender({ fitMap: state.photos.length === 1 }); } function textContent(node, namespace, localName) { @@ -1938,8 +1998,10 @@ function htmlPage(): string { "Import complete: " + loaded + " images imported" + (skipped ? ", " + skipped + " skipped" : "") + "." ); setProgress(listing.length, listing.length, "complete"); + flushVisiblePhotoRender({ fitMap: true }); } catch (error) { if (controller.signal.aborted) { + flushVisiblePhotoRender(); setImportStatus("Import stopped."); setProgress(state.processed, state.total, "canceled"); return; @@ -1950,6 +2012,7 @@ function htmlPage(): string { "Import failed: " + (error instanceof Error ? error.message : "unknown error"), "error" ); + flushVisiblePhotoRender(); } finally { activeImportController = null; setImporting(false);