Add resources download button + click to download picker in replay (#469)
Fixes #467 - Add a download button on the resource page for each entry to allow downloading each resource no matter what type. - Add a new 'click to download' image or media picker that allows highlighting + clicking on image/media to download on the page (to get around 'Save Image As...) not working. Also supports inline svgs + background images - should work better than Save Image As... - Location bar shows a popup indicating in 'click to download' mode, which can be canceled. Otherwise, mode remains until user downloads something. - Type fixes: Untangle Page and URLResource types for better type checking. - Deps: bump to wabac.js 2.25.1 for new download mode support --------- Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
This commit is contained in:
co-authored by
Tessa Walsh
parent
d2524ab26f
commit
916b154688
+90
-3
@@ -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)}"
|
||||
>
|
||||
</wr-coll-replay>
|
||||
`
|
||||
@@ -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` <span class="favicon icon is-small is-left">
|
||||
<img src="${this.favIconUrl}" />
|
||||
@@ -1258,6 +1291,23 @@ class Item extends LitElement {
|
||||
<p id="skip-replay-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
|
||||
}
|
||||
|
||||
protected renderClickToDownloadNotify() {
|
||||
return html`<article
|
||||
id="click-download-msg"
|
||||
class="loc-overlay has-background-link-light is-size-7"
|
||||
>
|
||||
<div class="ml-4 is-flex">
|
||||
Select image or media highlighted with 🟦 (blue box) on hover to
|
||||
download.
|
||||
</div>
|
||||
<button
|
||||
class="mx-4 delete"
|
||||
aria-label="delete"
|
||||
@click="${this.cancelClickToDownload}"
|
||||
></button>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
protected renderToolbarRight() {
|
||||
const isReplay = !!this.tabData.url;
|
||||
|
||||
@@ -1367,6 +1417,25 @@ class Item extends LitElement {
|
||||
<span>Purge Cache + Full Reload</span>
|
||||
</a>`
|
||||
: html``}
|
||||
${isReplay
|
||||
? html`<hr class="dropdown-divider" />
|
||||
<a
|
||||
@click="${this.clickToDownload}"
|
||||
role="button"
|
||||
class="dropdown-item"
|
||||
@keyup="${clickOnSpacebarPress}"
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<fa-icon
|
||||
size="1.0em"
|
||||
class="has-text-grey"
|
||||
aria-hidden="true"
|
||||
.svg="${fasFileDownload}"
|
||||
></fa-icon>
|
||||
</span>
|
||||
<span>Select Media to Download</span>
|
||||
</a>`
|
||||
: html``}
|
||||
${(!this.editable && this.downloadUrl?.startsWith("http://")) ||
|
||||
this.downloadUrl?.startsWith("https://")
|
||||
? html` <hr class="dropdown-divider" />
|
||||
@@ -1452,7 +1521,7 @@ class Item extends LitElement {
|
||||
const currDateStr = this.ts
|
||||
? dateTimeFormatter.format(tsToDate(this.ts) as Date)
|
||||
: "";
|
||||
return html`<div id="datetime" class="control is-hidden-mobile">
|
||||
return html`<div id="datetime" class="control is-hidden-mobile loc-overlay">
|
||||
${timestampStrs.length > 1
|
||||
? html`
|
||||
<sl-dropdown placement="top-end" hoist>
|
||||
@@ -1513,6 +1582,24 @@ class Item extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
clickToDownload() {
|
||||
const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
|
||||
if (replay) {
|
||||
this.clickToDownloadMode = true;
|
||||
replay.setClickToDownload();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
cancelClickToDownload() {
|
||||
const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
|
||||
if (replay) {
|
||||
this.clickToDownloadMode = false;
|
||||
replay.clearHilite(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
renderItemInfo() {
|
||||
if (!this.itemInfo)
|
||||
return html`<sl-alert open variant="warning">
|
||||
|
||||
+6
-5
@@ -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;
|
||||
}
|
||||
|
||||
+21
-15
@@ -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> | number[] | null = null;
|
||||
|
||||
@property({ type: Object })
|
||||
toDeletePage: URLResource | null = null;
|
||||
toDeletePage: Page | null = null;
|
||||
|
||||
@property({ type: Object })
|
||||
collInfo: ItemType | Record<string, never> | null = null;
|
||||
@@ -181,8 +181,7 @@ class Pages extends LitElement {
|
||||
this.sortKey = "date";
|
||||
this.sortDesc = true;
|
||||
}
|
||||
const sorter =
|
||||
this.renderRoot.querySelector<Sorter<URLResource>>("wr-sorter");
|
||||
const sorter = this.renderRoot.querySelector<Sorter<Page>>("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);
|
||||
|
||||
+20
-6
@@ -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,
|
||||
};
|
||||
|
||||
+282
-6
@@ -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<void> = 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 {
|
||||
</div>
|
||||
</div>`
|
||||
: html`
|
||||
<a
|
||||
href="${this.downloadResUrl}"
|
||||
@click="${this.hiliteClicked}"
|
||||
class="hilite-overlay"
|
||||
download="${ifDefined(
|
||||
this.downloadResUrl.startsWith("blob:")
|
||||
? "image.svg"
|
||||
: undefined,
|
||||
)}"
|
||||
></a>
|
||||
<div class="iframe-container">
|
||||
<iframe
|
||||
class="iframe-main"
|
||||
@@ -444,6 +472,254 @@ class Replay extends LitElement {
|
||||
</div>
|
||||
`}`;
|
||||
}
|
||||
|
||||
clearHilite(removeListeners = false) {
|
||||
if (this.hiliter) {
|
||||
this.hiliter.clearHilite(removeListeners);
|
||||
this.hiliter = null;
|
||||
}
|
||||
|
||||
if (removeListeners) {
|
||||
this.removeEventListener(
|
||||
"update-download-res-url",
|
||||
this.onUpdateDownloadResUrl,
|
||||
);
|
||||
this.dispatchEvent(new CustomEvent("cancel-click-download"));
|
||||
}
|
||||
}
|
||||
|
||||
hiliteClicked() {
|
||||
this.clearHilite(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
onUpdateDownloadResUrl(e: Event) {
|
||||
const { url } = (e as CustomEvent).detail;
|
||||
this.downloadResUrl = url;
|
||||
}
|
||||
|
||||
setClickToDownload() {
|
||||
if (this.hiliter) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addEventListener(
|
||||
"update-download-res-url",
|
||||
this.onUpdateDownloadResUrl,
|
||||
);
|
||||
|
||||
try {
|
||||
this.hiliter = new HoverHiliter(this);
|
||||
} catch (e) {
|
||||
this.hiliter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
class HoverHiliter {
|
||||
doc: Document;
|
||||
replay: Replay;
|
||||
|
||||
hiliteElem: HTMLElement | null = null;
|
||||
hiliteOverlay: HTMLElement;
|
||||
iframe: HTMLIFrameElement;
|
||||
|
||||
onMove: (event: MouseEvent) => void;
|
||||
onRecompute: () => void;
|
||||
|
||||
constructor(replay: Replay) {
|
||||
this.replay = replay;
|
||||
|
||||
const iframe = this.replay.renderRoot.querySelector("iframe");
|
||||
const hiliteOverlay =
|
||||
this.replay.renderRoot.querySelector(".hilite-overlay");
|
||||
|
||||
if (!hiliteOverlay || !iframe?.contentDocument) {
|
||||
throw new Error("missing elements");
|
||||
}
|
||||
|
||||
this.iframe = iframe;
|
||||
this.hiliteOverlay = hiliteOverlay as HTMLElement;
|
||||
|
||||
this.onMove = (event: MouseEvent) => this.hiliteOnMove(event);
|
||||
this.onRecompute = () => this.hiliteRecompute();
|
||||
|
||||
const doc = iframe.contentDocument;
|
||||
|
||||
this.doc = doc;
|
||||
|
||||
doc.addEventListener("mousemove", this.onMove);
|
||||
|
||||
doc.addEventListener("scroll", this.onRecompute);
|
||||
|
||||
doc.defaultView?.addEventListener("resize", this.onRecompute);
|
||||
}
|
||||
|
||||
clearHilite(removeListeners = false) {
|
||||
if (removeListeners) {
|
||||
this.doc.removeEventListener("mousemove", this.onMove);
|
||||
|
||||
this.doc.removeEventListener("scroll", this.onRecompute);
|
||||
|
||||
this.doc.defaultView?.removeEventListener("scroll", this.onRecompute);
|
||||
}
|
||||
this.hiliteElem = null;
|
||||
|
||||
this.hiliteOverlay.style.display = "none";
|
||||
}
|
||||
|
||||
hiliteRecompute() {
|
||||
if (!this.hiliteElem) {
|
||||
return;
|
||||
}
|
||||
const iframeRect = this.iframe.getBoundingClientRect();
|
||||
const elemRect = this.hiliteElem.getBoundingClientRect();
|
||||
|
||||
const offset = 10;
|
||||
|
||||
const leftX = iframeRect.left + window.scrollX + elemRect.left - offset;
|
||||
const topY = iframeRect.top + window.scrollY + elemRect.top - offset;
|
||||
|
||||
const hilite = this.hiliteOverlay;
|
||||
|
||||
hilite.style.left = leftX + "px";
|
||||
hilite.style.top = topY + "px";
|
||||
hilite.style.width = elemRect.width + offset * 2 + "px";
|
||||
hilite.style.height = elemRect.height + offset * 2 + "px";
|
||||
hilite.style.display = "block";
|
||||
}
|
||||
|
||||
hiliteOnMove(event: MouseEvent) {
|
||||
const elem = this.hiliteFindBestElement(event.clientX, event.clientY);
|
||||
|
||||
if (elem && this.hiliteElem === elem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getSrc = (elem: HTMLElement | null): string => {
|
||||
if (!elem) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if ((elem as HTMLImageElement).currentSrc) {
|
||||
return (elem as HTMLImageElement).currentSrc;
|
||||
}
|
||||
|
||||
if (elem.tagName === "image") {
|
||||
const href = elem.getAttribute("href");
|
||||
if (href) {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
if (elem.tagName === "svg") {
|
||||
if (!elem.getAttribute("xmlns")) {
|
||||
elem.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
}
|
||||
const buff = new TextEncoder().encode(elem.outerHTML);
|
||||
const blob = new Blob([buff], { type: "image/svg+xml" });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
const src = HTMLElement.prototype.getAttribute.call(elem, "src");
|
||||
if (src) {
|
||||
return src;
|
||||
}
|
||||
|
||||
if (elem.style.backgroundImage) {
|
||||
return elem.style.backgroundImage.replace(
|
||||
/(url\s*\(\s*[\\"']*)([^)'"]+)([\\"']*\s*\)?)/i,
|
||||
"$2",
|
||||
);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
const src = getSrc(elem);
|
||||
|
||||
if (src) {
|
||||
const newSrc = src.replace(
|
||||
/([\d]*)([\w][\w]_)(\/(https:|http:)?\/)/,
|
||||
"$1dl_$3",
|
||||
);
|
||||
this.replay.dispatchEvent(
|
||||
new CustomEvent("update-download-res-url", { detail: { url: newSrc } }),
|
||||
);
|
||||
this.hiliteElem = elem;
|
||||
this.hiliteRecompute();
|
||||
} else {
|
||||
this.clearHilite();
|
||||
}
|
||||
}
|
||||
|
||||
hiliteFindBestElement(x: number, y: number): HTMLElement | null {
|
||||
const elems = this.doc.elementsFromPoint(x, y) as HTMLElement[];
|
||||
|
||||
if (!elems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstElem = elems[0];
|
||||
|
||||
const containsRect = (rect: DOMRect) => {
|
||||
return (
|
||||
rect.x >= firstRect.x &&
|
||||
rect.y >= firstRect.y &&
|
||||
rect.width <= firstRect.width &&
|
||||
rect.height <= firstRect.height
|
||||
);
|
||||
};
|
||||
|
||||
// allow other elements only if they're within same bounding rect
|
||||
// as the deepest element
|
||||
const firstRect = firstElem.getBoundingClientRect();
|
||||
let isLast = false;
|
||||
for (const elem of elems) {
|
||||
const rect = elem.getBoundingClientRect();
|
||||
if (elem === firstElem || containsRect(rect)) {
|
||||
if (
|
||||
(elem as HTMLImageElement).currentSrc ||
|
||||
(elem.hasAttribute("src") && !(elem instanceof HTMLIFrameElement))
|
||||
) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
if (elem.tagName === "image" && elem.hasAttribute("href")) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
if (elem.tagName === "svg") {
|
||||
return elem;
|
||||
}
|
||||
} else {
|
||||
isLast = true;
|
||||
}
|
||||
|
||||
const subelem =
|
||||
elem.querySelector("[src]:not(script)") ||
|
||||
elem.querySelector(`div[style*="background-image: url("]`);
|
||||
if (subelem) {
|
||||
const subRect = subelem.getBoundingClientRect();
|
||||
if (
|
||||
containsRect(subRect) &&
|
||||
x >= subRect.left &&
|
||||
y >= subRect.top &&
|
||||
x < subRect.right &&
|
||||
y <= subRect.bottom
|
||||
) {
|
||||
return subelem as HTMLElement | null;
|
||||
}
|
||||
}
|
||||
|
||||
if (isLast) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-coll-replay", Replay);
|
||||
|
||||
+26
-12
@@ -1,21 +1,35 @@
|
||||
export type URLResource = {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
mime: string;
|
||||
status: string;
|
||||
date: Date;
|
||||
ts: string;
|
||||
desc?: string;
|
||||
timestamp?: string;
|
||||
size: number;
|
||||
favIconUrl: string;
|
||||
text?: string;
|
||||
waczhash?: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type Page = URLResource & {
|
||||
export type Page = {
|
||||
id: number;
|
||||
title: string;
|
||||
text?: string;
|
||||
url: string;
|
||||
mime: string;
|
||||
status: string;
|
||||
size: number;
|
||||
|
||||
ts: number;
|
||||
timestamp: string;
|
||||
date: Date | null;
|
||||
|
||||
favIconUrl?: string;
|
||||
waczhash?: string;
|
||||
seed?: boolean;
|
||||
isSeed?: boolean;
|
||||
desc?: string;
|
||||
};
|
||||
|
||||
export type URLTsChange = {
|
||||
url: string;
|
||||
ts: string;
|
||||
waczhash?: string;
|
||||
};
|
||||
|
||||
export type ItemType = {
|
||||
@@ -48,8 +62,8 @@ export type ItemType = {
|
||||
};
|
||||
onDemand?: boolean;
|
||||
pages: Page[];
|
||||
curatedPages: (URLResource & { list: string })[];
|
||||
lists: URLResource[];
|
||||
curatedPages: (Page & { list: string })[];
|
||||
lists: Page[];
|
||||
ctime?: string;
|
||||
totalSize?: unknown;
|
||||
size?: number | string;
|
||||
|
||||
+20
-1
@@ -1,9 +1,10 @@
|
||||
import { LitElement, html, css, type PropertyValues } from "lit";
|
||||
import { wrapCss, clickOnSpacebarPress } from "./misc";
|
||||
|
||||
import { getReplayLink } from "./pageutils";
|
||||
import { getReplayLink, getDownloadLink } from "./pageutils";
|
||||
|
||||
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
|
||||
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
|
||||
|
||||
import "keyword-mark-element/lib/keyword-mark.js";
|
||||
import { type ItemType } from "./types";
|
||||
@@ -315,6 +316,9 @@ class URLResources extends LitElement {
|
||||
.all-results .column {
|
||||
word-break: break-word;
|
||||
}
|
||||
.dl-button {
|
||||
padding: 4px;
|
||||
}
|
||||
div.sort-header {
|
||||
padding: 10px;
|
||||
margin-bottom: 0px !important;
|
||||
@@ -528,6 +532,21 @@ class URLResources extends LitElement {
|
||||
<tr class="columns result">
|
||||
<td class="column col-url is-6">
|
||||
<p class="minihead is-hidden-tablet">URL</p>
|
||||
<a
|
||||
class="dl-button"
|
||||
href="${getDownloadLink(
|
||||
this.collInfo!.replayPrefix,
|
||||
result.url,
|
||||
result.ts,
|
||||
)}"
|
||||
><fa-icon
|
||||
size="1.0em"
|
||||
class="has-text-black"
|
||||
aria-hidden="true"
|
||||
title="Download Resource"
|
||||
.svg="${fasDownload}"
|
||||
></fa-icon>
|
||||
</a>
|
||||
<a
|
||||
@click="${this.onReplay}"
|
||||
data-url="${result.url}"
|
||||
|
||||
Reference in New Issue
Block a user