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} OpenGraph image for {$entityInfo?.t == 'Url' && $entityInfo?.c} (loaded = address)} on:error={() => (handled = false)} /> @@ -108,9 +109,8 @@ {:else if types.audio}
Thumbnail for {address}... (loaded = address)} on:error={() => (handled = false)} /> @@ -129,11 +129,10 @@ {:else}
Thumbnail for {address}... (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(); + }, + }; +}