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 { when } from "lit/directives/when.js";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
|
|
||||||
|
import {
|
||||||
|
HomeView,
|
||||||
|
type CollectionSnapshotPreview,
|
||||||
|
} from "./collection-snapshot-preview";
|
||||||
import type { SelectSnapshotDetail } from "./select-collection-start-page";
|
import type { SelectSnapshotDetail } from "./select-collection-start-page";
|
||||||
|
|
||||||
import { BtrixElement } from "@/classes/BtrixElement";
|
import { BtrixElement } from "@/classes/BtrixElement";
|
||||||
import type { Dialog } from "@/components/ui/dialog";
|
import type { Dialog } from "@/components/ui/dialog";
|
||||||
import { formatRwpTimestamp } from "@/utils/replay";
|
|
||||||
|
|
||||||
enum HomeView {
|
|
||||||
Pages = "pages",
|
|
||||||
URL = "url",
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fires btrix-change
|
* @fires btrix-change
|
||||||
@ -76,11 +74,11 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
private readonly form?: HTMLFormElement | null;
|
private readonly form?: HTMLFormElement | null;
|
||||||
|
|
||||||
@query("#thumbnailPreview")
|
@query("#thumbnailPreview")
|
||||||
private readonly thumbnailPreview?: HTMLIFrameElement | null;
|
private readonly thumbnailPreview?: CollectionSnapshotPreview | null;
|
||||||
|
|
||||||
willUpdate(changedProperties: PropertyValues<this>) {
|
willUpdate(changedProperties: PropertyValues<this>) {
|
||||||
if (changedProperties.has("homeUrl") && this.homeUrl) {
|
if (changedProperties.has("homeUrl")) {
|
||||||
this.homeView = HomeView.URL;
|
this.homeView = this.homeUrl ? HomeView.URL : HomeView.Pages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +87,7 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
this.homeView === HomeView.URL && !this.selectedSnapshot;
|
this.homeView === HomeView.URL && !this.selectedSnapshot;
|
||||||
return html`
|
return html`
|
||||||
<btrix-dialog
|
<btrix-dialog
|
||||||
.label=${msg("Configure Replay Home")}
|
.label=${msg("Configure Replay View")}
|
||||||
.open=${this.open}
|
.open=${this.open}
|
||||||
class="[--width:60rem]"
|
class="[--width:60rem]"
|
||||||
@sl-show=${() => (this.showContent = true)}
|
@sl-show=${() => (this.showContent = true)}
|
||||||
@ -142,11 +140,6 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderPreview() {
|
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 =
|
const snapshot =
|
||||||
this.selectedSnapshot ||
|
this.selectedSnapshot ||
|
||||||
(this.homeUrl
|
(this.homeUrl
|
||||||
@ -154,22 +147,19 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
url: this.homeUrl,
|
url: this.homeUrl,
|
||||||
ts: this.homeTs,
|
ts: this.homeTs,
|
||||||
pageId: this.homePageId,
|
pageId: this.homePageId,
|
||||||
|
status: 200,
|
||||||
}
|
}
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
if (snapshot) {
|
const replaySource = `/api/orgs/${this.orgId}/collections/${this.collectionId}/replay.json`;
|
||||||
urlPreview = html`
|
// TODO Get query from replay-web-page embed
|
||||||
<sl-tooltip hoist>
|
const query = queryString.stringify({
|
||||||
<iframe
|
source: replaySource,
|
||||||
class="inline-block size-full"
|
customColl: this.collectionId,
|
||||||
id="thumbnailPreview"
|
embed: "default",
|
||||||
src=${`/replay/w/${this.collectionId}/${formatRwpTimestamp(snapshot.ts)}id_/urn:thumbnail:${snapshot.url}`}
|
noCache: 1,
|
||||||
>
|
noSandbox: 1,
|
||||||
</iframe>
|
});
|
||||||
<span slot="content" class="break-all">${snapshot.url}</span>
|
|
||||||
</sl-tooltip>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@ -177,13 +167,15 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
? "flex items-center justify-center"
|
? "flex items-center justify-center"
|
||||||
: ""} relative aspect-video overflow-hidden rounded-lg border bg-slate-50"
|
: ""} relative aspect-video overflow-hidden rounded-lg border bg-slate-50"
|
||||||
>
|
>
|
||||||
${when(
|
<btrix-collection-snapshot-preview
|
||||||
this.homeView === HomeView.URL && this.replayLoaded,
|
class="contents"
|
||||||
() => urlPreview,
|
id="thumbnailPreview"
|
||||||
)}
|
collectionId=${this.collectionId || ""}
|
||||||
<div class="${this.homeView === HomeView.URL ? "offscreen" : ""}">
|
view=${this.homeView}
|
||||||
${this.renderReplay()}
|
replaySrc=${`/replay/?${query}#view=pages`}
|
||||||
</div>
|
.snapshot=${snapshot}
|
||||||
|
>
|
||||||
|
</btrix-collection-snapshot-preview>
|
||||||
|
|
||||||
${when(
|
${when(
|
||||||
!this.replayLoaded,
|
!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) {
|
private async onSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const form = e.currentTarget as HTMLFormElement;
|
const form = e.currentTarget as HTMLFormElement;
|
||||||
const { homeView, useThumbnail } = serialize(form);
|
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
|
// No changes to save
|
||||||
this.open = false;
|
this.open = false;
|
||||||
return;
|
return;
|
||||||
@ -322,8 +298,6 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
(homeView === HomeView.URL && this.selectedSnapshot?.pageId) || null,
|
(homeView === HomeView.URL && this.selectedSnapshot?.pageId) || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.dispatchEvent(new CustomEvent("btrix-change"));
|
|
||||||
|
|
||||||
const shouldUpload =
|
const shouldUpload =
|
||||||
homeView === HomeView.URL &&
|
homeView === HomeView.URL &&
|
||||||
useThumbnail === "on" &&
|
useThumbnail === "on" &&
|
||||||
@ -333,19 +307,13 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
const fileName = `page-thumbnail_${this.selectedSnapshot?.pageId}.jpeg`;
|
const fileName = `page-thumbnail_${this.selectedSnapshot?.pageId}.jpeg`;
|
||||||
let file: File | undefined;
|
let file: File | undefined;
|
||||||
|
|
||||||
if (shouldUpload && this.thumbnailPreview?.src) {
|
if (shouldUpload && this.thumbnailPreview) {
|
||||||
const { src } = this.thumbnailPreview;
|
const blob = await this.thumbnailPreview.thumbnailBlob;
|
||||||
|
|
||||||
// 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 (blob) {
|
||||||
file = new File([blob], fileName, {
|
file = new File([blob], fileName, {
|
||||||
type: blob.type,
|
type: blob.type,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
console.debug(err);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.notify.toast({
|
this.notify.toast({
|
||||||
@ -387,6 +355,8 @@ export class CollectionStartPageDialog extends BtrixElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new CustomEvent("btrix-change"));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug(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=${() => {
|
@sl-after-hide=${() => {
|
||||||
this.tabGroup?.show(Tab.Link);
|
this.tabGroup?.show(Tab.Link);
|
||||||
}}
|
}}
|
||||||
class="[--width:40rem] [--body-spacing:0]"
|
class="[--body-spacing:0] [--width:40rem]"
|
||||||
>
|
>
|
||||||
<sl-tab-group>
|
<sl-tab-group>
|
||||||
<sl-tab slot="nav" panel=${Tab.Link}
|
<sl-tab slot="nav" panel=${Tab.Link}
|
||||||
|
@ -167,10 +167,14 @@ export class CollectionDetail extends BtrixElement {
|
|||||||
<sl-button
|
<sl-button
|
||||||
size="small"
|
size="small"
|
||||||
@click=${() => (this.openDialogName = "editStartPage")}
|
@click=${() => (this.openDialogName = "editStartPage")}
|
||||||
?disabled=${!this.collection?.crawlCount}
|
?disabled=${!this.collection?.crawlCount ||
|
||||||
|
!this.isRwpLoaded}
|
||||||
>
|
>
|
||||||
<sl-icon name="house-gear" slot="prefix"></sl-icon>
|
${!this.collection ||
|
||||||
${msg("Configure Home")}
|
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-button>
|
||||||
</sl-tooltip>
|
</sl-tooltip>
|
||||||
`,
|
`,
|
||||||
@ -400,8 +404,8 @@ export class CollectionDetail extends BtrixElement {
|
|||||||
}}
|
}}
|
||||||
?disabled=${!this.collection?.crawlCount}
|
?disabled=${!this.collection?.crawlCount}
|
||||||
>
|
>
|
||||||
<sl-icon name="house-gear" slot="prefix"></sl-icon>
|
<sl-icon name="gear" slot="prefix"></sl-icon>
|
||||||
${msg("Configure Replay Home")}
|
${msg("Configure Replay View")}
|
||||||
</sl-menu-item>
|
</sl-menu-item>
|
||||||
<sl-menu-item
|
<sl-menu-item
|
||||||
@click=${async () => {
|
@click=${async () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user