import { localized, msg } from "@lit/localize"; import { Task } from "@lit/task"; import clsx from "clsx"; import { html, nothing, type PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { isEqual } from "lodash"; import type { SelectSnapshotDetail } from "./select-collection-page"; import { TailwindElement } from "@/classes/TailwindElement"; import { formatRwpTimestamp } from "@/utils/replay"; import { tw } from "@/utils/tailwind"; export enum HomeView { Pages = "pages", URL = "url", } export type BtrixValidateDetail = { valid: boolean; }; /** * Display preview of page snapshot. * * A previously loaded `replay-web-page` embed is required in order for preview to work. */ @customElement("btrix-collection-snapshot-preview") @localized() export class CollectionSnapshotPreview extends TailwindElement { @property({ type: String }) collectionId = ""; @property({ type: String }) replaySrc = ""; @property({ type: String }) view?: HomeView; @property({ type: Boolean }) noSpinner = false; @property({ type: Object, hasChanged: (a, b) => !isEqual(a, b), }) snapshot?: Partial; @query("iframe") private readonly iframe?: HTMLIFrameElement | null; @query("img#preview") private readonly previewImg?: HTMLImageElement | null; @state() private iframeLoaded = false; // Set up a promise and a helper callback so that we can wait until the iframe is loaded, rather than returning nothing when it's not yet loaded private iframeLoadComplete!: () => void; private readonly iframeLoadedPromise = new Promise((res) => { if (this.iframeLoaded) res(); this.iframeLoadComplete = res; }); public get thumbnailBlob() { return this.blobTask.taskComplete.then(() => this.blobTask.value); } public readonly blobTask = new Task(this, { task: async ([collectionId, snapshot], { signal }) => { try { console.debug("waiting for iframe to load", { collectionId, snapshot }); await this.iframeLoadedPromise; if ( !collectionId || !snapshot?.ts || !snapshot.url || !this.iframe?.contentWindow ) { console.debug( "exiting early due to missing props", collectionId, snapshot, this.iframe?.contentWindow, ); return; } const resp = await this.iframe.contentWindow.fetch( `/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`, { signal }, ); if (resp.status === 200) { this.dispatchEvent( new CustomEvent("btrix-validate", { detail: { valid: true }, }), ); return await resp.blob(); } throw new Error(`couldn't get thumbnail`); } catch (e) { console.error(e); if (signal.aborted) return; this.dispatchEvent( new CustomEvent("btrix-validate", { detail: { valid: false }, }), ); throw e; } }, args: () => [this.collectionId, this.snapshot] as const, }); @state() private prevObjUrl?: string; private readonly objectUrlTask = new Task(this, { task: ([blob]) => { this.prevObjUrl = this.objectUrlTask.value; if (!blob) return ""; const url = URL.createObjectURL(blob); if (url) return url; throw new Error("no object url"); }, args: () => [this.blobTask.value] as const, }); disconnectedCallback(): void { super.disconnectedCallback(); if (this.objectUrlTask.value) { URL.revokeObjectURL(this.objectUrlTask.value); } } protected willUpdate(changedProperties: PropertyValues): void { if ( changedProperties.has("collectionId") || changedProperties.has("snapshot") ) { // revoke object urls once the `` element has loaded the next url, to // prevent flashes this.previewImg?.addEventListener("load", () => { if (this.prevObjUrl) { URL.revokeObjectURL(this.prevObjUrl); this.prevObjUrl = undefined; } }); } } render() { return html` ${this.renderSnapshot()} ${this.renderReplay()} `; } private renderSnapshot() { if (this.view === HomeView.Pages) return; return this.blobTask.render({ complete: this.renderImage, pending: this.renderImage, error: this.renderError, }); } private readonly renderImage = () => { if (!this.snapshot) { if (this.noSpinner) return; return html`

${msg("Enter a Page URL to preview it.")}

`; } return html`
${this.objectUrlTask.value ? nothing : this.renderSpinner()}
${this.prevObjUrl ? html`` : nothing} ${this.objectUrlTask.value ? html`` : nothing}
${this.snapshot.url}
`; }; private renderReplay() { return html`
`; } private readonly renderError = () => html`

${msg("This page doesn’t have a preview. Try another URL or timestamp.")}

`; private readonly renderSpinner = () => { if (this.noSpinner) return; return html`
`; }; }