import { LitElement, html, css, type CSSResultGroup, type TemplateResult, type PropertyValues, } from "lit"; import { customElement, property } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js"; import { wrapCss, IS_APP, VERSION, clickOnSpacebarPress, updateFaviconLinks, } from "./misc"; import brandLockupColor from "~assets/brand/replaywebpage-lockup-color.svg"; import rwpLogo from "~assets/brand/replaywebpage-icon-color.svg"; import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.svg"; import fasArrowLeft from "@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg"; import fasArrowRight from "@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg"; import { SWManager } from "./swmanager"; import "./item"; import "./item-index"; import "./chooser"; import { type EmbedOpts, type LoadInfo } from "./item"; import type { FavIconEventDetail } from "./types"; // =========================================================================== @customElement("replay-app-main") export class ReplayWebApp extends LitElement { @property({ type: Boolean }) inited = false; @property({ type: Object }) pageParams: URLSearchParams = new URLSearchParams(); @property({ type: String }) sourceUrl: string | null = null; @property({ type: Boolean }) navMenuShown = false; @property({ type: Boolean }) showAbout = false; @property({ type: Boolean }) showFileDropOverlay = false; @property({ type: String }) collTitle: string | null = null; @property({ type: Object }) loadInfo: LoadInfo | null = null; @property({ type: String }) embed: string | null = null; @property({ type: Object }) embedOpts: EmbedOpts = {}; @property({ type: String }) collPageUrl = ""; @property({ type: String }) pageTitle = ""; @property({ type: Boolean }) pageReplay = false; @property({ type: String }) source: string | null = null; @property({ type: Boolean }) skipRuffle = false; @property({ type: String }) swErrorMsg: TemplateResult<1> | string | null = null; protected swName?: string; protected swmanager: SWManager | null; private useRuffle = false; private droppedFile: File | null = null; constructor(swName = __SW_NAME__) { super(); this.swName = swName; this.swmanager = null; this.skipRuffle = false; this.useRuffle = false; this.safariKeyframes(); this.addEventListener("dragenter", (dragEvent) => { this.maybeStartFileDrop(dragEvent); }); this.addEventListener("dragover", (dragEvent) => { this.maybeStartFileDrop(dragEvent); }); this.addEventListener("dragleave", () => { this.showFileDropOverlay = false; }); this.addEventListener("dragend", () => { this.showFileDropOverlay = false; }); this.addEventListener("drop", (dragEvent) => { this.droppedFile = dragEvent.dataTransfer?.files[0] || null; this.showFileDropOverlay = false; dragEvent.preventDefault(); }); } private readonly maybeStartFileDrop = (dragEvent: DragEvent) => { if (this.sourceUrl) { // A source is already loaded. Don't allow dropping a file. return; } this.showFileDropOverlay = true; dragEvent.preventDefault(); }; get appName() { return "ReplayWeb.page"; } get homeUrl() { return window.location.pathname; } static get styles() { return wrapCss(ReplayWebApp.appStyles); } static get appStyles(): CSSResultGroup { return css` #wrlogo { max-height: 1rem; } .navbar { height: 1.5rem; } .navbar-brand { height: 1.5rem; display: flex; align-items: center; } .wr-logo-item { padding: 0 0.5rem 0 0; -webkit-user-drag: none; -khtml-user-drag: none; -moz-user-drag: none; -o-user-drag: none; user-drag: none; } .no-wrap { white-space: nowrap; overflow-x: hidden; text-overflow: ellipsis; } .has-allcaps { font-variant-caps: small-caps; } :host { position: fixed; left: 0px; top: 0px; bottom: 0px; right: 0px; display: flex; min-width: 0px; flex-direction: column; } wr-item { flex: 1 1 auto; overflow: hidden; } .navbar { padding: 0 0.5em; } div.navbar-menu fa-icon { vertical-align: sub; } .tagline { margin-top: 1rem; } .drop-file-overlay { position: relative; display: flex; align-items: center; justify-content: center; position: fixed; inset: 0; z-index: 50; font-weight: bold; font-size: 1.5rem; background: rgba(255, 255, 255, 0.5); backdrop-filter: blur(2px); } .drop-file-overlay:after { pointer-events: none; content: " "; position: absolute; inset: 0; border: 5px dashed #aaa; margin: 15px; } @media screen and (min-width: 840px) { .menu-only { display: none; } a.arrow-button { padding-left: 4px; padding-right: 4px; } .info-menu { padding: 0 1em; } .logo-text { padding-left: 0px; margin-left: 6px; } a.navbar-item.logo-text:hover { background-color: initial; } } @media screen and (max-width: 840px) { .wide-only { display: none !important; } } `; } get mainLogo() { return rwpLogo as string; } renderNavBrand() { return html` `; } renderNavBar() { return html`

Skipped

`; } renderNavEnd() { return html`  User Docs About `; } renderColl() { return html` (this.showAbout = true)} >`; } renderHomeIndex() { return html` ${!IS_APP ? html`

Explore and Replay Interactive Archived Webpages Directly in your Browser. (Learn More)

` : ""}
`; } render() { if (!this.inited) { return html``; } if (this.embed && this.swErrorMsg) { return this.swErrorMsg; } return html` ${!this.embed || this.embed === "full" ? this.renderNavBar() : ""} ${this.sourceUrl ? this.renderColl() : this.renderHomeIndex()} ${this.showAbout ? this.renderAbout() : ""} ${this.showFileDropOverlay ? this.renderDropFileOverlay() : ""} `; } firstUpdated() { this.initRoute(); // service worker name let name = this.swName; const qp = new URLSearchParams(); let query = ""; if (this.useRuffle) { qp.set("injectScripts", "ruffle/ruffle.js"); qp.set("allowProxyPaths", "ruffle/"); } if (this.embed) { qp.set("serveIndex", "1"); } query = qp.toString(); if (query.length) { name += "?" + query; } this.swmanager = new SWManager({ name, appName: this.appName }); this.swmanager.register().catch( () => // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1. (this.swErrorMsg = this.swmanager?.renderErrorReport()), ); window.addEventListener("popstate", () => { this.initRoute(); }); } updated(changedProperties: PropertyValues) { if (changedProperties.has("sourceUrl")) { this.collTitle = null; } } onFavIcons(event: CustomEvent) { updateFaviconLinks(event.detail); } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. skipMenu(event) { // This is a workaround, since this app's routing doesn't permit normal // following of in-page anchors. event.preventDefault(); this.renderRoot.querySelector("#skip-main-target")?.focus(); } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. onNavMenu(event) { event.preventDefault(); event.stopPropagation(); this.navMenuShown = !this.navMenuShown; if (this.navMenuShown) { // Since this menu can be large and obscure significant page content, // dismiss like a modal, returning keyboard focus to the // menu button on dismissal. document.addEventListener( "click", (event) => { event.preventDefault(); this.navMenuShown = false; this.renderRoot.querySelector("#menu-button")?.focus(); }, { once: true }, ); document.addEventListener( "keypress", (event) => { if (event.key == "Escape") { event.preventDefault(); this.navMenuShown = false; this.renderRoot.querySelector("#menu-button")?.focus(); } }, { once: true }, ); } } initRoute() { this.inited = true; this.pageParams = new URLSearchParams(window.location.search); // Google Drive const state = this.pageParams.get("state"); type State = { ids?: string[]; userId?: string; action?: string; }; if (state) { try { const parsedState: State = JSON.parse(state); if ( parsedState.ids instanceof Array && parsedState.userId && parsedState.action === "open" ) { this.pageParams.set("source", "googledrive://" + parsedState.ids[0]); this.pageParams.delete("state"); window.location.search = this.pageParams.toString(); return; } } catch (e) { console.log(e); } } if (this.source) { this.pageParams.set("source", this.source); const url = new URL(window.location.href); url.search = this.pageParams.toString(); window.history.replaceState({}, document.title, url.href); } this.sourceUrl = this.pageParams.get("source") || ""; this.embed = this.pageParams.get("embed") || ""; if (!this.embed) { this.useRuffle = !this.skipRuffle; } else { this.useRuffle = this.pageParams.get("ruffle") === "1"; } if (this.pageParams.has("terms")) { this.showAbout = true; } if (this.pageParams.has("embed")) { if (!this.loadInfo) { this.loadInfo = {}; } if (this.pageParams.has("noMediaDownload")) { this.embedOpts = { ...this.embedOpts, noMediaDownloadUI: true }; } } if (this.pageParams.get("config")) { try { this.loadInfo!.extraConfig = JSON.parse( this.pageParams.get("config") || "", ); } catch (e) { console.log("invalid config: " + e); } } if (this.pageParams.get("baseUrlSourcePrefix")) { this.loadInfo!.extraConfig = this.loadInfo!.extraConfig || {}; this.loadInfo!.extraConfig.baseUrlSourcePrefix = this.pageParams.get( "baseUrlSourcePrefix", ); } if (this.pageParams.get("basePageUrl")) { this.loadInfo!.extraConfig = this.loadInfo!.extraConfig || {}; this.loadInfo!.extraConfig.baseUrl = this.pageParams.get("basePageUrl"); } if (this.pageParams.get("adblockUrl")) { this.loadInfo!.extraConfig = this.loadInfo!.extraConfig || {}; this.loadInfo!.extraConfig.adblockUrl = this.pageParams.get("adblockUrl"); } if (this.pageParams.get("customColl")) { this.loadInfo!.customColl = this.pageParams.get("customColl"); } if (this.pageParams.get("noWebWorker") === "1") { this.loadInfo!.noWebWorker = true; } if (this.pageParams.get("noCache") === "1") { this.loadInfo!.noCache = true; } if (this.pageParams.get("hideOffscreen") === "1") { this.loadInfo!.hideOffscreen = true; } if (this.pageParams.get("loading") === "eager") { this.loadInfo!.loadEager = true; } if (this.pageParams.get("swName")) { this.swName = this.pageParams.get("swName") || undefined; } } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. onStartLoad(event) { // just redirect right away? // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.pageParams.set("source", event.detail.sourceUrl); const url = new URL(window.location.href); url.search = this.pageParams.toString(); this.collPageUrl = url.toString(); if (!event.detail.isFile) { window.location.search = this.pageParams.toString(); return; } else { window.history.pushState({}, "", this.collPageUrl); } this.sourceUrl = event.detail.sourceUrl; this.loadInfo = event.detail; } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. onCollLoaded(event) { this.loadInfo = null; if (event.detail.collInfo) { this.collTitle = event.detail.collInfo.name || event.detail.collInfo.title; } if (event.detail.alreadyLoaded) { //this.sourceLoaded = true; return; } //this.initRoute(); if (event.detail.sourceUrl !== this.sourceUrl) { // TODO: Fix this the next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.pageParams.set("source", event.detail.sourceUrl); window.location.search = this.pageParams.toString(); } } private onCollLoadCancel() { this.sourceUrl = null; this.loadInfo = null; window.history.pushState({}, "", new URL(window.location.href).origin); } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. onTitle(event) { if (event.detail.title) { this.pageTitle = event.detail.title; this.pageReplay = event.detail.replayTitle; document.title = (event.detail.replayTitle ? "Archive of " : "") + this.pageTitle + " | " + this.appName; } } safariKeyframes() { // Safari requires keyframes at the document level, add spinner keyframes here const css = ` @keyframes spinAround { from { transform: rotate(0deg); } to { transform: rotate(359deg); } } `; const style = document.createElement("style"); document.head.appendChild(style); style.appendChild(document.createTextNode(css)); } renderAbout() { return html` `; } onAboutClose() { this.showAbout = false; } renderDropFileOverlay() { return html`
Drop to load web archive
`; } }