fix: Fully load thumbnail before save (#2307)
Fixes https://github.com/webrecorder/browsertrix/issues/2306 ## Changes Refactors collection view configuration to wait for thumbnail preview image (using `URL.createObjectURL`, like in QA screenshots) to be fully loaded from `replay-web-page` before saving.
This commit is contained in:
parent
4583babecb
commit
a64f3a6c4c
@ -6,16 +6,14 @@ import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { when } from "lit/directives/when.js";
|
||||
import queryString from "query-string";
|
||||
|
||||
import {
|
||||
HomeView,
|
||||
type CollectionSnapshotPreview,
|
||||
} from "./collection-snapshot-preview";
|
||||
import type { SelectSnapshotDetail } from "./select-collection-start-page";
|
||||
|
||||
import { BtrixElement } from "@/classes/BtrixElement";
|
||||
import type { Dialog } from "@/components/ui/dialog";
|
||||
import { formatRwpTimestamp } from "@/utils/replay";
|
||||
|
||||
enum HomeView {
|
||||
Pages = "pages",
|
||||
URL = "url",
|
||||
}
|
||||
|
||||
/**
|
||||
* @fires btrix-change
|
||||
@ -76,11 +74,11 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
private readonly form?: HTMLFormElement | null;
|
||||
|
||||
@query("#thumbnailPreview")
|
||||
private readonly thumbnailPreview?: HTMLIFrameElement | null;
|
||||
private readonly thumbnailPreview?: CollectionSnapshotPreview | null;
|
||||
|
||||
willUpdate(changedProperties: PropertyValues<this>) {
|
||||
if (changedProperties.has("homeUrl") && this.homeUrl) {
|
||||
this.homeView = HomeView.URL;
|
||||
if (changedProperties.has("homeUrl")) {
|
||||
this.homeView = this.homeUrl ? HomeView.URL : HomeView.Pages;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +87,7 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
this.homeView === HomeView.URL && !this.selectedSnapshot;
|
||||
return html`
|
||||
<btrix-dialog
|
||||
.label=${msg("Configure Replay Home")}
|
||||
.label=${msg("Configure Replay View")}
|
||||
.open=${this.open}
|
||||
class="[--width:60rem]"
|
||||
@sl-show=${() => (this.showContent = true)}
|
||||
@ -142,11 +140,6 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
}
|
||||
|
||||
private renderPreview() {
|
||||
let urlPreview = html`
|
||||
<p class="m-3 text-pretty text-neutral-500">
|
||||
${msg("Enter a Page URL to preview it")}
|
||||
</p>
|
||||
`;
|
||||
const snapshot =
|
||||
this.selectedSnapshot ||
|
||||
(this.homeUrl
|
||||
@ -154,22 +147,19 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
url: this.homeUrl,
|
||||
ts: this.homeTs,
|
||||
pageId: this.homePageId,
|
||||
status: 200,
|
||||
}
|
||||
: null);
|
||||
|
||||
if (snapshot) {
|
||||
urlPreview = html`
|
||||
<sl-tooltip hoist>
|
||||
<iframe
|
||||
class="inline-block size-full"
|
||||
id="thumbnailPreview"
|
||||
src=${`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`}
|
||||
>
|
||||
</iframe>
|
||||
<span slot="content" class="break-all">${snapshot.url}</span>
|
||||
</sl-tooltip>
|
||||
`;
|
||||
}
|
||||
const replaySource = `/api/orgs/${this.orgId}/collections/${this.collectionId}/replay.json`;
|
||||
// TODO Get query from replay-web-page embed
|
||||
const query = queryString.stringify({
|
||||
source: replaySource,
|
||||
customColl: this.collectionId,
|
||||
embed: "default",
|
||||
noCache: 1,
|
||||
noSandbox: 1,
|
||||
});
|
||||
|
||||
return html`
|
||||
<div
|
||||
@ -177,13 +167,15 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
? "flex items-center justify-center"
|
||||
: ""} relative aspect-video overflow-hidden rounded-lg border bg-slate-50"
|
||||
>
|
||||
${when(
|
||||
this.homeView === HomeView.URL && this.replayLoaded,
|
||||
() => urlPreview,
|
||||
)}
|
||||
<div class="${this.homeView === HomeView.URL ? "offscreen" : ""}">
|
||||
${this.renderReplay()}
|
||||
</div>
|
||||
<btrix-collection-snapshot-preview
|
||||
class="contents"
|
||||
id="thumbnailPreview"
|
||||
collectionId=${this.collectionId || ""}
|
||||
view=${this.homeView}
|
||||
replaySrc=${`/replay/?${query}#view=pages`}
|
||||
.snapshot=${snapshot}
|
||||
>
|
||||
</btrix-collection-snapshot-preview>
|
||||
|
||||
${when(
|
||||
!this.replayLoaded,
|
||||
@ -281,34 +273,18 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderReplay() {
|
||||
const replaySource = `/api/orgs/${this.orgId}/collections/${this.collectionId}/replay.json`;
|
||||
// TODO Get query from replay-web-page embed
|
||||
const query = queryString.stringify({
|
||||
source: replaySource,
|
||||
customColl: this.collectionId,
|
||||
embed: "default",
|
||||
noCache: 1,
|
||||
noSandbox: 1,
|
||||
});
|
||||
|
||||
return html`<div class="aspect-video w-[200%]">
|
||||
<div class="pointer-events-none aspect-video origin-top-left scale-50">
|
||||
<iframe
|
||||
class="inline-block size-full"
|
||||
src=${`/replay/?${query}#view=pages`}
|
||||
></iframe>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private async onSubmit(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.currentTarget as HTMLFormElement;
|
||||
const { homeView, useThumbnail } = serialize(form);
|
||||
|
||||
if (homeView === HomeView.Pages && !this.homePageId) {
|
||||
if (
|
||||
(homeView === HomeView.Pages && !this.homePageId) ||
|
||||
(homeView === HomeView.URL &&
|
||||
this.selectedSnapshot &&
|
||||
this.homePageId === this.selectedSnapshot.pageId)
|
||||
) {
|
||||
// No changes to save
|
||||
this.open = false;
|
||||
return;
|
||||
@ -322,8 +298,6 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
(homeView === HomeView.URL && this.selectedSnapshot?.pageId) || null,
|
||||
});
|
||||
|
||||
this.dispatchEvent(new CustomEvent("btrix-change"));
|
||||
|
||||
const shouldUpload =
|
||||
homeView === HomeView.URL &&
|
||||
useThumbnail === "on" &&
|
||||
@ -333,19 +307,13 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
const fileName = `page-thumbnail_${this.selectedSnapshot?.pageId}.jpeg`;
|
||||
let file: File | undefined;
|
||||
|
||||
if (shouldUpload && this.thumbnailPreview?.src) {
|
||||
const { src } = this.thumbnailPreview;
|
||||
|
||||
// Wait to get the thumbnail image before closing the dialog
|
||||
try {
|
||||
const resp = await this.thumbnailPreview.contentWindow!.fetch(src);
|
||||
const blob = await resp.blob();
|
||||
if (shouldUpload && this.thumbnailPreview) {
|
||||
const blob = await this.thumbnailPreview.thumbnailBlob;
|
||||
|
||||
if (blob) {
|
||||
file = new File([blob], fileName, {
|
||||
type: blob.type,
|
||||
});
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
}
|
||||
} else {
|
||||
this.notify.toast({
|
||||
@ -387,6 +355,8 @@ export class CollectionStartPageDialog extends BtrixElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent("btrix-change"));
|
||||
} catch (err) {
|
||||
console.debug(err);
|
||||
|
||||
|
172
frontend/src/features/collections/collection-snapshot-preview.ts
Normal file
172
frontend/src/features/collections/collection-snapshot-preview.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { localized, msg } from "@lit/localize";
|
||||
import { Task } from "@lit/task";
|
||||
import clsx from "clsx";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
|
||||
import type { SelectSnapshotDetail } from "./select-collection-start-page";
|
||||
|
||||
import { TailwindElement } from "@/classes/TailwindElement";
|
||||
import { formatRwpTimestamp } from "@/utils/replay";
|
||||
import { tw } from "@/utils/tailwind";
|
||||
|
||||
export enum HomeView {
|
||||
Pages = "pages",
|
||||
URL = "url",
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: Object })
|
||||
snapshot?: SelectSnapshotDetail["item"];
|
||||
|
||||
@query("iframe")
|
||||
private readonly iframe?: HTMLIFrameElement | null;
|
||||
|
||||
@state()
|
||||
private iframeLoaded = false;
|
||||
|
||||
public get thumbnailBlob() {
|
||||
return this.blobTask.taskComplete.finally(() => this.blobTask.value);
|
||||
}
|
||||
|
||||
private readonly blobTask = new Task(this, {
|
||||
task: async ([collectionId, snapshot, iframeLoaded]) => {
|
||||
if (
|
||||
!collectionId ||
|
||||
!snapshot ||
|
||||
!iframeLoaded ||
|
||||
!this.iframe?.contentWindow
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resp = await this.iframe.contentWindow.fetch(
|
||||
`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`,
|
||||
);
|
||||
|
||||
if (resp.status === 200) {
|
||||
return await resp.blob();
|
||||
}
|
||||
|
||||
throw new Error(`couldn't get thumbnail`);
|
||||
},
|
||||
args: () => [this.collectionId, this.snapshot, this.iframeLoaded] as const,
|
||||
});
|
||||
|
||||
private readonly objectUrlTask = new Task(this, {
|
||||
task: ([blob]) => {
|
||||
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")
|
||||
) {
|
||||
if (this.objectUrlTask.value) {
|
||||
URL.revokeObjectURL(this.objectUrlTask.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${this.renderSnapshot()} ${this.renderReplay()} `;
|
||||
}
|
||||
|
||||
private renderSnapshot() {
|
||||
if (this.view === HomeView.Pages) return;
|
||||
|
||||
return this.blobTask.render({
|
||||
complete: this.renderImage,
|
||||
pending: this.renderSpinner,
|
||||
error: this.renderError,
|
||||
});
|
||||
}
|
||||
|
||||
private readonly renderImage = () => {
|
||||
if (!this.snapshot) {
|
||||
return html`
|
||||
<p class="m-3 text-pretty text-neutral-500">
|
||||
${msg("Enter a Page URL to preview it")}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="size-full">
|
||||
<sl-tooltip hoist>
|
||||
${this.objectUrlTask.render({
|
||||
complete: (value) =>
|
||||
value
|
||||
? html`<img class="size-full" src=${value} />`
|
||||
: this.renderSpinner(),
|
||||
pending: () => "pending",
|
||||
})}
|
||||
<span slot="content" class="break-all">${this.snapshot.url}</span>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
private renderReplay() {
|
||||
return html`<div
|
||||
class=${clsx(tw`size-full`, this.view === HomeView.URL && tw`offscreen`)}
|
||||
>
|
||||
<div class="aspect-video w-[200%]">
|
||||
<div class="pointer-events-none aspect-video origin-top-left scale-50">
|
||||
<iframe
|
||||
class="inline-block size-full"
|
||||
src=${this.replaySrc}
|
||||
@load=${() => {
|
||||
this.iframeLoaded = true;
|
||||
}}
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private readonly renderError = () => html`
|
||||
<p class="m-3 text-pretty text-danger">
|
||||
${msg("Couldn't load preview. Try another snapshot")}
|
||||
</p>
|
||||
`;
|
||||
|
||||
private readonly renderSpinner = () => html`
|
||||
<div class="flex size-full items-center justify-center text-2xl">
|
||||
<sl-spinner></sl-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
@ -246,7 +246,7 @@ export class ShareCollection extends BtrixElement {
|
||||
@sl-after-hide=${() => {
|
||||
this.tabGroup?.show(Tab.Link);
|
||||
}}
|
||||
class="[--width:40rem] [--body-spacing:0]"
|
||||
class="[--body-spacing:0] [--width:40rem]"
|
||||
>
|
||||
<sl-tab-group>
|
||||
<sl-tab slot="nav" panel=${Tab.Link}
|
||||
|
@ -167,10 +167,14 @@ export class CollectionDetail extends BtrixElement {
|
||||
<sl-button
|
||||
size="small"
|
||||
@click=${() => (this.openDialogName = "editStartPage")}
|
||||
?disabled=${!this.collection?.crawlCount}
|
||||
?disabled=${!this.collection?.crawlCount ||
|
||||
!this.isRwpLoaded}
|
||||
>
|
||||
<sl-icon name="house-gear" slot="prefix"></sl-icon>
|
||||
${msg("Configure Home")}
|
||||
${!this.collection ||
|
||||
Boolean(this.collection.crawlCount && !this.isRwpLoaded)
|
||||
? html`<sl-spinner slot="prefix"></sl-spinner>`
|
||||
: html`<sl-icon name="gear" slot="prefix"></sl-icon>`}
|
||||
${msg("Configure View")}
|
||||
</sl-button>
|
||||
</sl-tooltip>
|
||||
`,
|
||||
@ -400,8 +404,8 @@ export class CollectionDetail extends BtrixElement {
|
||||
}}
|
||||
?disabled=${!this.collection?.crawlCount}
|
||||
>
|
||||
<sl-icon name="house-gear" slot="prefix"></sl-icon>
|
||||
${msg("Configure Replay Home")}
|
||||
<sl-icon name="gear" slot="prefix"></sl-icon>
|
||||
${msg("Configure Replay View")}
|
||||
</sl-menu-item>
|
||||
<sl-menu-item
|
||||
@click=${async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user