diff --git a/CHANGES.md b/CHANGES.md index 8f8e182..2b13d5c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,12 @@ ## CHANGES +*From release 2.4.0+, changes will now be listed in the release notes on GitHub.* +*Please see [the list of releases](https://github.com/webrecorder/replayweb.page/releases) for an up-to-date +list of changes for each release.* + +--- + v2.3.23 - Fidelity: Fix possible regression from different JS parser in wabac.js, additional XHR improvements (viw abac.js 2.24.5) diff --git a/package.json b/package.json index e9ef1c9..029695f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "replaywebpage", "productName": "ReplayWeb.page", - "version": "2.3.23", + "version": "2.4.0", "description": "Serverless Web Archive Replay", "repository": "https://github.com/webrecorder/replayweb.page", "homepage": "https://replayweb.page/", @@ -22,7 +22,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@shoelace-style/shoelace": "~2.15.1", - "@webrecorder/wabac": "^2.24.5", + "@webrecorder/wabac": "^2.25.1", "bulma": "^0.9.3", "electron-log": "^4.4.1", "electron-updater": "^6.6.2", diff --git a/src/item.ts b/src/item.ts index 628581d..bc8ab3d 100644 --- a/src/item.ts +++ b/src/item.ts @@ -28,6 +28,7 @@ import fasTriangleExclamation from "@fortawesome/fontawesome-free/svgs/solid/exc import fasBook from "@fortawesome/fontawesome-free/svgs/solid/book.svg"; import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg"; +import fasFileDownload from "@fortawesome/fontawesome-free/svgs/regular/arrow-alt-circle-down.svg"; import farListAlt from "@fortawesome/fontawesome-free/svgs/regular/list-alt.svg"; import farResources from "@fortawesome/fontawesome-free/svgs/solid/puzzle-piece.svg"; @@ -190,6 +191,9 @@ class Item extends LitElement { @property({ type: Array }) multiTs?: string[] = []; + @property({ type: Boolean }) + clickToDownloadMode = false; + private splitter: Split.Instance | null = null; private _replaceLoc = false; @@ -743,8 +747,31 @@ class Item extends LitElement { line-height: 2; } + #click-download-msg { + position: absolute; + right: 0.5rem; + z-index: 10; + background: #09c1ff; + top: 5px; + bottom: 5px; + margin-right: -3px; + border-radius: 8px; + display: flex; + align-items: center; + line-height: 2; + } + + #hilite-sample { + width: 20px; + height: 20px; + background-color: rgba(0, 0, 255, 0.5); + border: solid 4px blue; + margin: 2px; + display: inline-flex; + } + /* Gradient to indicate URL clipping */ - #datetime:before { + .loc-overlay:before { content: ""; position: absolute; top: 0; @@ -988,6 +1015,8 @@ class Item extends LitElement { id="replay" @replay-loading="${this.onReplayLoading}" @replay-favicons="${this.onFavIcons}" + @cancel-click-download="${() => + (this.clickToDownloadMode = false)}" > ` @@ -1244,7 +1273,11 @@ class Item extends LitElement { .value="${this.url}" placeholder="Enter text to search or a URL to replay" /> - ${isReplay ? this.renderTimestamp() : ""} + ${isReplay + ? this.clickToDownloadMode + ? this.renderClickToDownloadNotify() + : this.renderTimestamp() + : ""} ${showFavIcon ? html` @@ -1258,6 +1291,23 @@ class Item extends LitElement {

Skipped

`; } + protected renderClickToDownloadNotify() { + return html``; + } + protected renderToolbarRight() { const isReplay = !!this.tabData.url; @@ -1367,6 +1417,25 @@ class Item extends LitElement { Purge Cache + Full Reload ` : html``} + ${isReplay + ? html` + + + + + Select Media to Download + ` + : html``} ${(!this.editable && this.downloadUrl?.startsWith("http://")) || this.downloadUrl?.startsWith("https://") ? html` @@ -1452,7 +1521,7 @@ class Item extends LitElement { const currDateStr = this.ts ? dateTimeFormatter.format(tsToDate(this.ts) as Date) : ""; - return html`
+ return html`
${timestampStrs.length > 1 ? html` @@ -1513,6 +1582,24 @@ class Item extends LitElement { } } + clickToDownload() { + const replay = this.renderRoot.querySelector("wr-coll-replay"); + if (replay) { + this.clickToDownloadMode = true; + replay.setClickToDownload(); + } + return false; + } + + cancelClickToDownload() { + const replay = this.renderRoot.querySelector("wr-coll-replay"); + if (replay) { + this.clickToDownloadMode = false; + replay.clearHilite(true); + } + return false; + } + renderItemInfo() { if (!this.itemInfo) return html` diff --git a/src/pageentry.ts b/src/pageentry.ts index 5e01e04..05d56e9 100644 --- a/src/pageentry.ts +++ b/src/pageentry.ts @@ -8,7 +8,7 @@ import "keyword-mark-element/lib/keyword-mark.js"; import { getPageDateTS, getReplayLink } from "./pageutils"; import { wrapCss } from "./misc"; -import type { URLResource } from "./types"; +import type { Page, URLTsChange } from "./types"; import { dateFormatter, dateTimeFormatter, @@ -24,7 +24,7 @@ class PageEntry extends LitElement { textSnippet: string | null = ""; @property({ type: Object }) - page: URLResource | null = null; + page: Page | null = null; @property({ type: String }) replayPrefix = ""; @@ -241,7 +241,7 @@ class PageEntry extends LitElement { href="${getReplayLink( "pages", this.page!.url, - this.page!.timestamp!, + this.page!.timestamp, this.page!.waczhash, )}" > @@ -294,7 +294,7 @@ class PageEntry extends LitElement { private getReplayPrefix() { const waczhash = this.page!.waczhash ? `:${this.page!.waczhash}/` : ""; - const ts = this.page!.timestamp || ""; + const ts = this.page!.ts || ""; return this.replayPrefix + "/" + waczhash + ts + "id_"; } @@ -370,11 +370,12 @@ class PageEntry extends LitElement { onReplay(event: Event, reload = false) { event.preventDefault(); - const data = { + const data: URLTsChange = { url: this.page!.url, ts: this.page!.timestamp, waczhash: this.page!.waczhash, }; + this.sendChangeEvent(data, reload); return false; } diff --git a/src/pages.ts b/src/pages.ts index 54d6a57..29b8b4c 100644 --- a/src/pages.ts +++ b/src/pages.ts @@ -18,17 +18,17 @@ import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.svg"; import type { Sorter } from "./sorter"; import type { PageEntry } from "./pageentry"; import type { Id, Index } from "flexsearch"; -import type { ItemType, URLResource } from "./types"; +import type { ItemType, Page } from "./types"; const DYNAMIC_PAGE_SIZE = 25; // =========================================================================== class Pages extends LitElement { @property({ type: Array }) - filteredPages: URLResource[] = []; + filteredPages: Page[] = []; @property({ type: Array }) - sortedPages: URLResource[] = []; + sortedPages: Page[] = []; @property({ type: String }) query = ""; @@ -37,7 +37,7 @@ class Pages extends LitElement { flex: Index | null = null; @property({ attribute: false }) - textPages: URLResource[] | null = null; + textPages: Page[] | null = null; @property() newQuery: string | null = null; @@ -94,7 +94,7 @@ class Pages extends LitElement { toDeletePages: Set | number[] | null = null; @property({ type: Object }) - toDeletePage: URLResource | null = null; + toDeletePage: Page | null = null; @property({ type: Object }) collInfo: ItemType | Record | null = null; @@ -181,8 +181,7 @@ class Pages extends LitElement { this.sortKey = "date"; this.sortDesc = true; } - const sorter = - this.renderRoot.querySelector>("wr-sorter"); + const sorter = this.renderRoot.querySelector>("wr-sorter"); if (sorter) { sorter.sortKey = this.sortKey; sorter.sortDesc = this.sortDesc; @@ -236,7 +235,9 @@ class Pages extends LitElement { if (this.dynamicPagesQuery) { if (!this.query) { - const seedPages = this.collInfo!.pages.filter((x) => x.isSeed); + const seedPages = this.collInfo!.pages.filter( + (x) => x.isSeed || x.seed, + ); this.filteredPages = this.showAllPages || !seedPages.length ? [...this.collInfo!.pages] @@ -256,9 +257,6 @@ class Pages extends LitElement { // normalize the date for (const page of this.filteredPages) { const { timestamp, date } = getPageDateTS(page); - if (date == null) { - throw new Error("Page date is null"); - } page.timestamp = timestamp; page.date = date; } @@ -292,7 +290,7 @@ class Pages extends LitElement { const knownPages = new Set(); this.filteredPages.forEach((x) => knownPages.add(x.id)); - const newPages = []; + const newPages: Page[] = []; for (const { id, @@ -304,6 +302,7 @@ class Pages extends LitElement { favIconUrl, waczhash, isSeed, + size, } of json.pages) { if (knownPages.has(id)) { continue; @@ -313,16 +312,20 @@ class Pages extends LitElement { continue; } - let tsActual; + let tsActual: number; + let date: Date; if (typeof ts === "string") { + date = new Date(ts); tsActual = new Date(ts).getTime(); } else { tsActual = ts; + date = new Date(tsActual); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newPage: any = { + const timestamp = getTS(date.toISOString()); + + const newPage: Page = { id, url, title, @@ -331,6 +334,9 @@ class Pages extends LitElement { ts: tsActual, favIconUrl, waczhash, + timestamp, + size, + date, }; newPages.push(newPage); diff --git a/src/pageutils.ts b/src/pageutils.ts index 592a78f..b0133be 100644 --- a/src/pageutils.ts +++ b/src/pageutils.ts @@ -1,4 +1,4 @@ -import type { URLResource } from "./types"; +import type { Page } from "./types"; // =========================================================================== async function digestMessage(message: string, hashtype: AlgorithmIdentifier) { @@ -50,16 +50,17 @@ function getDateFromTS(ts: string | number) { } // =========================================================================== -function getPageDateTS(page: URLResource) { +function getPageDateTS(page: Page) { let date: Date | null = null; try { - date = new Date(page.ts || page.date); + if (page.ts > 0) { + date = new Date(page.ts); + } } catch (e) { - // leave date unchanged in case of error + // ignore } - const timestamp = - date && date instanceof Date ? getTS(date.toISOString()) : ""; + const timestamp = date ? getTS(date.toISOString()) : ""; return { date, timestamp }; } @@ -85,6 +86,18 @@ function getReplayLink( return "#" + params.toString(); } +// =========================================================================== +function getDownloadLink( + replayPrefix: string, + url: string, + ts: string, + waczhash?: string, +) { + return `${replayPrefix}/${waczhash ? `:${waczhash}/` : ""}${ + ts || "" + }dl_/${url}`; +} + // =========================================================================== async function sourceToId(url: string) { try { @@ -137,6 +150,7 @@ export { getPageDateTS, getDateFromTS, getReplayLink, + getDownloadLink, sourceToId, parseURLSchemeHostPath, }; diff --git a/src/replay.ts b/src/replay.ts index 56a32f7..a2de375 100644 --- a/src/replay.ts +++ b/src/replay.ts @@ -1,9 +1,10 @@ import { LitElement, html, css, type PropertyValues } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; import { property } from "lit/decorators.js"; import { wrapCss } from "./misc"; import rwpLogo from "~assets/brand/replaywebpage-icon-color.svg"; -import type { ItemType } from "./types"; +import type { ItemType, URLTsChange } from "./types"; // =========================================================================== class Replay extends LitElement { @@ -49,10 +50,15 @@ class Replay extends LitElement { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch authFileHandle: any = null; + @property({ type: String }) + downloadResUrl = ""; + private reauthWait: null | Promise = null; private _loadPoll: null | number = null; + private hiliter: HoverHiliter | null = null; + firstUpdated() { window.addEventListener("message", (event) => this.onReplayMessage(event)); // TODO: Fix this the next time the file is edited. @@ -143,9 +149,10 @@ class Replay extends LitElement { (this.replayUrl && changedProperties.has("replayUrl")) || (this.replayTS && changedProperties.has("replayTS")) ) { - const data = { + const data: URLTsChange = { url: this.replayUrl, ts: this.replayTS, + waczhash: this.waczhash, }; this.dispatchEvent( @@ -203,7 +210,7 @@ class Replay extends LitElement { this.replayUrl = event.data.url; this.title = event.data.title || this.title; this.replayNotFoundError = event.data.wb_type === "archive-not-found"; - this.clearLoading(iframe.contentWindow); + this.clearLoading(iframe); if (event.data.icons) { const icons = event.data.icons; @@ -265,13 +272,12 @@ class Replay extends LitElement { (iframe.contentDocument.readyState === "complete" && !(iframe.contentWindow as Window & { _WBWombat: unknown })._WBWombat) ) { - this.clearLoading(iframe?.contentWindow); + this.clearLoading(iframe); } }, 5000); } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'iframeWin' implicitly has an 'any' type. - clearLoading(iframeWin) { + clearLoading(iframe: HTMLIFrameElement | null) { this.dispatchEvent( new CustomEvent("replay-loading", { detail: { loading: false } }), ); @@ -281,6 +287,8 @@ class Replay extends LitElement { this._loadPoll = null; } + const iframeWin = iframe?.contentWindow; + if (iframeWin) { try { iframeWin.addEventListener("beforeunload", () => { @@ -293,6 +301,7 @@ class Replay extends LitElement { } setLoading() { + this.clearHilite(true); this.dispatchEvent( new CustomEvent("replay-loading", { detail: { loading: true } }), ); @@ -367,6 +376,15 @@ class Replay extends LitElement { margin: 3em; background-color: white; } + + .hilite-overlay { + display: none; + position: absolute; + z-index: 9999; + background-color: rgba(0, 0, 255, 0.5); + border: solid 10px blue; + cursor: crosshair; + } `); } @@ -388,6 +406,16 @@ class Replay extends LitElement {
` : html` +