SOURCE / PINNED RELEASE
Made of little things.
Black Site Map Editor
- Release
- 32a57733b7e9…
- Author-recorded commit
- 3f54890f542f…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor
Archive hash verified: 7a4b1abbac7db5b0…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { html, type PropertyValues } from 'lit';
import { resource } from '@napplet/sdk';
import { LightElement } from './light-element';
import { loadMap, type MapRecord } from '../map-host';
import { renderMap } from '../map-renderer';
import { mapSize } from '../model';
import { runtimeHasDomain } from '../domain-availability';
// Bound simultaneous downloads while a large gallery scrolls into view.
let active = 0;
const waiting: (() => void)[] = [];
async function slot<T>(task: () => Promise<T>): Promise<T> {
if (active >= 4) await new Promise<void>((resolve) => waiting.push(resolve));
active++;
try {
return await task();
} finally {
active--;
waiting.shift()?.();
}
}
export class BsMapPreview extends LightElement {
static properties = {
record: { attribute: false },
url: { state: true },
message: { state: true },
};
declare record: MapRecord;
declare url?: string;
declare message: string;
constructor() {
super();
this.message = 'Loading preview…';
}
#observer?: IntersectionObserver;
#version = 0;
connectedCallback() {
super.connectedCallback();
this.#observer = new IntersectionObserver((entries) => {
if (entries.some((e) => e.isIntersecting)) {
this.#observer?.disconnect();
void this.#load();
}
});
this.#observer.observe(this);
}
updated(changed: PropertyValues) {
if (changed.has('record') && changed.get('record')) {
if (this.url) URL.revokeObjectURL(this.url);
this.url = undefined;
this.message = 'Loading preview…';
void this.#load();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.#version++;
this.#observer?.disconnect();
if (this.url) URL.revokeObjectURL(this.url);
}
async #load() {
const version = ++this.#version;
await slot(async () => {
if (!this.isConnected || version !== this.#version) return;
try {
if (!runtimeHasDomain('resource')) throw new Error();
const image = this.record.metadata.image;
if (image) {
try {
const blob = await resource.bytes(image);
if (
![
'image/png',
'image/jpeg',
'image/webp',
'image/gif',
'image/avif',
].includes(blob.type) ||
blob.size > 5 * 1024 * 1024
)
throw new Error();
if (version !== this.#version) return;
this.url = URL.createObjectURL(blob);
await this.updateComplete;
return;
} catch {
/* An unavailable cover still gets a verified map preview. */
}
}
const map = await loadMap(this.record);
if (version !== this.#version) return;
await this.updateComplete;
const canvas = this.querySelector('canvas')!;
const [w, h] = mapSize(map);
canvas.width = 640;
canvas.height = Math.max(160, Math.round((640 * h) / w));
renderMap(canvas.getContext('2d')!, map, { tileSize: 640 / w });
this.message = '';
} catch {
if (version === this.#version) this.message = 'Preview unavailable';
}
});
}
render() {
return html`${
this.url
? html`<img
src=${this.url}
alt=${`Preview of ${this.record.metadata.title}`}
@error=${() => {
if (this.url) URL.revokeObjectURL(this.url);
this.url = undefined;
this.message = 'Preview unavailable';
}}
/>`
: html`<canvas
aria-label=${`Preview of ${this.record?.metadata.title ?? 'map'}`}
?hidden=${Boolean(this.message)}
></canvas
>${this.message ? html`<span>${this.message}</span>` : ''}`
}`;
}
}
customElements.define('bs-map-preview', BsMapPreview);
