import { LitElement, html, css, type PropertyValues, nothing } from "lit"; import { IS_APP, wrapCss } from "./misc"; import rwpLogo from "~assets/brand/replaywebpage-icon-color.svg"; import rwpLogoAnimated from "~assets/brand/replaywebpage-icon-color-animated.svg"; import prettyBytes from "pretty-bytes"; import { property } from "lit/decorators.js"; import type { LoadInfo } from "./item"; import { ifDefined } from "lit/directives/if-defined.js"; import { serviceWorkerActivated } from "./swmanager"; // =========================================================================== /** * @fires coll-load-cancel */ type LoadingState = | "started" | "waiting" | "googledrive" | "errored" | "permission_needed"; const NO_ANIM_STATES: LoadingState[] = [ "errored", "googledrive", "permission_needed", ]; declare let window: Window & { electron?: { getFileLoadUrl: (sourceUrl: string) => string; }; }; class Loader extends LitElement { @property({ type: String }) sourceUrl?: string; @property({ type: Object }) loadInfo: LoadInfo | null = null; @property({ type: String }) state: LoadingState = "waiting"; @property({ type: Number }) progress = 0; @property({ type: Number }) percent = 0; @property({ type: Number }) currentSize = 0; @property({ type: Number }) totalSize = 0; @property({ type: String }) error?: string; @property({ type: Number }) total = 0; @property({ type: String }) status?: string; @property({ type: String }) coll = ""; @property({ type: String }) embed?: string; @property({ type: Boolean }) tryFileHandle = !!window.showOpenFilePicker; @property({ type: Boolean }) errorAllowRetry = false; @property({ type: String }) extraMsg?: string; @property({ type: String }) swName?: string; pingInterval: number | NodeJS.Timeout = 0; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch fileHandle: any = null; noWebWorker = false; worker?: Worker | null; // Google Drive _gdWait?: Promise; _gdResolve!: (value: LoadInfo | PromiseLike) => void; firstUpdated() { this.initMessages(); //this.doLoad(); } initMessages() { this.noWebWorker = Boolean(this.loadInfo && this.loadInfo.noWebWorker); if (!this.noWebWorker) { this.worker = new Worker(this.swName!); } else { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!navigator.serviceWorker) { return; } // TODO figure out if this is okay? this.worker = navigator.serviceWorker as unknown as Worker; } this.worker.addEventListener( "message", ( event: MessageEvent<{ msg_type: string; name: string; percent: number; error?: string; fileHandle: FileSystemHandle | null; currentSize?: number; totalSize?: number; extraMsg?: string; }>, ) => { switch (event.data.msg_type) { case "collProgress": if (event.data.name === this.coll) { this.percent = event.data.percent; if (event.data.error) { this.error = event.data.error; this.state = "errored"; this.errorAllowRetry = true; this.fileHandle = event.data.fileHandle; if (this.error === "missing_local_file") { this.tryFileHandle = false; } else if ( this.error === "permission_needed" && event.data.fileHandle ) { this.state = "permission_needed"; break; } } if (event.data.currentSize && event.data.totalSize) { this.currentSize = event.data.currentSize; this.totalSize = event.data.totalSize; } this.extraMsg = event.data.extraMsg; } break; case "collAdded": if (event.data.name === this.coll) { if (!this.total) { this.total = 100; } this.progress = this.total; this.percent = 100; this.dispatchEvent( new CustomEvent("coll-loaded", { detail: event.data }), ); if (!this.noWebWorker) { this.worker?.terminate(); } else { if (this.pingInterval) { clearInterval(this.pingInterval); } } this.worker = null; } break; } }, ); } async doLoad() { let sourceUrl = this.sourceUrl; let source: LoadInfo | null = null; this.percent = this.currentSize = this.totalSize = 0; // const noSWError = getSWErrorMsg(); if (this.loadInfo?.swError) { this.state = "errored"; this.error = this.loadInfo.swError; this.errorAllowRetry = false; return; } // custom protocol handlers here... try { const url = new URL(sourceUrl!); const scheme = url.protocol.slice(0, -1); switch (scheme) { case "googledrive": this.state = "googledrive"; // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition source = (await this.googledriveInit()) ?? null; break; case "s3": source = { sourceUrl, loadUrl: `https://${url.hostname}.s3.amazonaws.com${url.href.slice( url.hostname.length + "s3://".length, )}`, name: this.sourceUrl, }; break; case "file": if (!this.loadInfo && sourceUrl && IS_APP && window.electron) { const loadUrl = window.electron.getFileLoadUrl(sourceUrl); const name = loadUrl.slice(loadUrl.lastIndexOf("/") + 1); source = { sourceUrl, loadUrl, name, noCache: true, }; } else { if (!this.loadInfo && !this.tryFileHandle) { this.state = "errored"; this.error = `\ File URLs can not be entered directly or shared. You can select a file to upload from the main page by clicking the 'Choose File...' button.`; this.errorAllowRetry = false; return; } source = this.loadInfo; } break; case "proxy": sourceUrl = "proxy:" + sourceUrl!.slice("proxy://".length); break; } } catch (e) { console.log(e); } if (!source) { source = { sourceUrl }; } this.state = "started"; let type = undefined; let extraConfig: LoadInfo["extraConfig"] = undefined; if (this.loadInfo) { source.newFullImport = this.loadInfo.newFullImport; source.loadEager = this.loadInfo.loadEager; source.noCache = this.loadInfo.noCache; if (this.loadInfo.extraConfig) { extraConfig = this.loadInfo.extraConfig; } // todo: too special case? if (sourceUrl!.startsWith("proxy:") && extraConfig?.recording) { type = "recordingproxy"; } } const msg = { msg_type: "addColl", name: this.coll, extraConfig, type, skipExisting: true, file: source, }; await serviceWorkerActivated(); if (this.worker) { if (!this.noWebWorker) { this.worker.postMessage(msg); } else { // @ts-expect-error - TS2531 - Object is possibly 'null'. navigator.serviceWorker.controller.postMessage(msg); // ping service worker with messages to avoid shutdown while loading // (mostly for Firefox) this.pingInterval = setInterval(() => { // @ts-expect-error - TS2531 - Object is possibly 'null'. navigator.serviceWorker.controller.postMessage({ msg_type: "ping" }); }, 15000); } } } // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/promise-function-async googledriveInit() { this._gdWait = new Promise((resolve) => (this._gdResolve = resolve)); return this._gdWait; } onLoadReady(event: CustomEvent) { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (this._gdResolve) { //const digest = await digestMessage(url, 'SHA-256'); //this.coll = "id-" + digest.slice(0, 12); // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this._gdResolve(event.detail); } } async onCancel() { if (!this.worker) { return; } const msg = { msg_type: "cancelLoad", name: this.coll }; if (!this.noWebWorker) { this.worker.postMessage(msg); await this.updateComplete; this.dispatchEvent( new CustomEvent("coll-load-cancel", { bubbles: true, composed: true, }), ); return; } // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (navigator.serviceWorker?.controller) { navigator.serviceWorker.controller.postMessage(msg); if (this.pingInterval) { clearInterval(this.pingInterval); } } } updated(changedProperties: PropertyValues) { if ( Boolean(this.sourceUrl && changedProperties.has("sourceUrl")) || changedProperties.has("tryFileHandle") ) { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.doLoad(); } } static get styles() { return wrapCss(css` :host { height: 100%; display: flex; } .progress-div { position: relative; width: 400px !important; } .progress-label { position: absolute; top: 0; left: 50%; transform: translateX(-50%); font-size: calc(1.5rem / 1.5); line-height: 1.5rem; } .loaded-prog { margin-bottom: 1em; } .error { white-space: pre-wrap; margin-bottom: 2em; } section.container { margin: auto; } .extra-msg { font-size: 0.8rem; } `); } render() { return html`
${!this.embed ? html`

Loading ${this.sourceUrl}...

` : ""}
${this.renderContent()}
`; } renderContent() { switch (this.state) { case "googledrive": return html``; case "started": return html`
${!this.currentSize ? nothing : this.renderProgressBar()} ${!this.embed ? html` ` : ""}
`; case "errored": return html`
${this.error}
${this.errorAllowRetry ? html` window.parent.location.reload()} >Try Again` : ""} ${this.embed ? html`` : html` Back`}
`; case "permission_needed": return html`
Permission is needed to reload the archive file. (Click Cancel to cancel loading this archive.)
Cancel
`; case "waiting": default: return html``; } } private renderProgressBar() { // Calculate percentage based on currentSize and totalSize // if data is available before actual percent const percent = this.currentSize && this.totalSize ? Math.max(this.percent, (this.currentSize / this.totalSize) * 100) : this.percent; // Round up <1 percentages const displayPercent = percent ? Math.max(percent, 1) : undefined; return html` ${displayPercent ? html` ` : nothing} ${this.currentSize && this.totalSize ? html`
Loaded ${prettyBytes(this.currentSize)} of ${prettyBytes(this.totalSize)} ${this.extraMsg && html`

(${this.extraMsg})

`}
` : html``} `; } async onAskPermission() { const result = await this.fileHandle?.requestPermission({ mode: "read", }); if (result === "granted") { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.doLoad(); } } } customElements.define("wr-loader", Loader); export { Loader };