diff --git a/webui/src/components/display/BlobPreview.svelte b/webui/src/components/display/BlobPreview.svelte
index 7ad9e57..2ad77c0 100644
--- a/webui/src/components/display/BlobPreview.svelte
+++ b/webui/src/components/display/BlobPreview.svelte
@@ -9,6 +9,7 @@
import { createEventDispatcher } from "svelte";
import { getTypes } from "../../util/mediatypes";
import { formatDuration } from "../../util/fragments/time";
+ import { concurrentImage } from "../imageQueue";
const dispatch = createEventDispatcher();
export let address: string;
@@ -94,8 +95,8 @@
/>
{:else if types.web}
(loaded = address)}
on:error={() => (handled = false)}
/>
@@ -108,9 +109,8 @@
{:else if types.audio}
![Thumbnail for {address}...]({api.apiUrl}/thumb/{address}?mime=audio)
(loaded = address)}
on:error={() => (handled = false)}
/>
@@ -129,11 +129,10 @@
{:else}
![Thumbnail for {address}...]({api.apiUrl}/{types.mimeType?.includes('svg+xml')
- ? 'raw'
- : 'thumb'}/{address}?size=512&quality=75)
(loaded = address)}
on:error={() => (handled = false)}
/>
diff --git a/webui/src/components/imageQueue.ts b/webui/src/components/imageQueue.ts
new file mode 100644
index 0000000..bdfdaae
--- /dev/null
+++ b/webui/src/components/imageQueue.ts
@@ -0,0 +1,123 @@
+import debug from "debug";
+const dbg = debug("upend:imageQueue");
+
+class ImageQueue {
+ concurrency: number;
+ queue: {
+ element: HTMLElement;
+ id: string;
+ callback: () => Promise
;
+ check?: () => boolean;
+ }[] = [];
+ active = 0;
+
+ constructor(concurrency: number) {
+ this.concurrency = concurrency;
+ }
+
+ public add(
+ element: HTMLImageElement,
+ id: string,
+ callback: () => Promise,
+ order?: () => number,
+ check?: () => boolean
+ ) {
+ this.queue = this.queue.filter((e) => e.element !== element);
+ this.queue.push({ element, id, callback, check });
+ this.update();
+ }
+
+ private update() {
+ this.queue.sort((a, b) => {
+ const aBox = a.element.getBoundingClientRect();
+ const bBox = b.element.getBoundingClientRect();
+ const topDifference = aBox.top - bBox.top;
+ if (topDifference !== 0) {
+ return topDifference;
+ } else {
+ return aBox.left - bBox.left;
+ }
+ });
+
+ dbg(
+ "Active: %d, Queue: %O",
+ this.active,
+ this.queue.map((e) => [e.element, e.id])
+ );
+
+ if (this.active >= this.concurrency) {
+ return;
+ }
+ if (!this.queue.length) {
+ return;
+ }
+ const nextIdx = this.queue.findIndex((e) => e.check()) || 0;
+ const next = this.queue.splice(nextIdx, 1)[0];
+ dbg(`Getting ${next.id}...`);
+ this.active += 1;
+ next
+ .callback()
+ .then(() => {
+ dbg(`Loaded ${next.id}`);
+ })
+ .catch(() => {
+ dbg(`Failed to load ${next.id}...`);
+ })
+ .finally(() => {
+ this.active -= 1;
+ this.update();
+ });
+ }
+}
+
+const imageQueue = new ImageQueue(1);
+
+export function concurrentImage(element: HTMLImageElement, src: string) {
+ const bbox = element.getBoundingClientRect();
+ let visible =
+ bbox.top >= 0 &&
+ bbox.left >= 0 &&
+ bbox.bottom <= window.innerHeight &&
+ bbox.right <= window.innerWidth;
+
+ const observer = new IntersectionObserver((entries) => {
+ visible = entries.some((e) => e.isIntersecting);
+ });
+ observer.observe(element);
+
+ function queueSelf() {
+ const loadSelf = () => {
+ return new Promise((resolve, reject) => {
+ if (element.src === src) {
+ resolve();
+ return;
+ }
+ element.addEventListener("load", () => {
+ resolve();
+ });
+ element.addEventListener("error", () => {
+ reject();
+ });
+ element.src = src;
+ });
+ };
+
+ imageQueue.add(
+ element,
+ src,
+ loadSelf,
+ () => element.getBoundingClientRect().top,
+ () => visible
+ );
+ }
+ queueSelf();
+
+ return {
+ update(_src: string) {
+ queueSelf();
+ },
+ destroy() {
+ observer.disconnect();
+ },
+ };
+}