Back to Black Site Map Editor
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.

src/components/bs-map-preview.ts
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);