Format src files with Prettier (#215)

Formats files in `src` with [prettier for consistent code style](https://prettier.io/docs/en/why-prettier) and adds npm script

Related to https://github.com/webrecorder/replayweb.page/issues/201
This commit is contained in:
sua yoo
2023-08-16 18:21:18 -07:00
committed by GitHub
parent 32b2dad06e
commit 1893550274
26 changed files with 3567 additions and 2277 deletions
+4 -13
View File
@@ -3,7 +3,10 @@ module.exports = {
"browser": true,
"es6": true
},
"extends": "eslint:recommended",
"extends": [
"eslint:recommended",
"prettier"
],
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
@@ -13,21 +16,9 @@ module.exports = {
2,
"event", "error"
],
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};
+4
View File
@@ -0,0 +1,4 @@
# Ignore everything except src
*
!/src/
!src/*
+4 -1
View File
@@ -34,9 +34,11 @@
"electron-builder": "^23.0.3",
"electron-notarize": "^1.1.1",
"eslint": "^8.23.1",
"eslint-config-prettier": "^9.0.0",
"http-server": "^13.0.2",
"mini-css-extract-plugin": "^2.3.0",
"node-sass": "^9.0.0",
"prettier": "^3.0.1",
"raw-loader": "^4.0.2",
"sass-loader": "^13.3.2",
"style-loader": "^3.2.1",
@@ -67,7 +69,8 @@
"dist": "yarn run build && yarn run pack",
"dist-dev": "yarn run build-dev && yarn run pack",
"release": "yarn run build && electron-builder",
"lint": "eslint ./src/ webpack.config.js"
"lint": "eslint ./src/ webpack.config.js",
"format": "prettier --write ./src"
},
"build": {
"afterSign": "build/notarize.js",
+206 -76
View File
@@ -1,15 +1,19 @@
import { LitElement, html, css } from "lit";
import { wrapCss, rwpLogo, IS_APP, VERSION, clickOnSpacebarPress } from "./misc";
import {
wrapCss,
rwpLogo,
IS_APP,
VERSION,
clickOnSpacebarPress,
} from "./misc";
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";
// ===========================================================================
class ReplayWebApp extends LitElement
{
class ReplayWebApp extends LitElement {
// eslint-disable-next-line no-undef
constructor(swName = __SW_NAME__) {
super();
@@ -105,7 +109,7 @@ class ReplayWebApp extends LitElement
static get appStyles() {
return css`
#wrlogo {
max-height: 1.0rem;
max-height: 1rem;
}
.navbar {
height: 1.5rem;
@@ -147,7 +151,7 @@ class ReplayWebApp extends LitElement
vertical-align: sub;
}
.tagline {
margin-top: 1.0rem;
margin-top: 1rem;
}
.drop-file-overlay {
@@ -160,7 +164,7 @@ class ReplayWebApp extends LitElement
z-index: 50;
font-weight: bold;
font-size: 1.5rem;
background: rgba(255,255,255,.5);
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(2px);
}
@@ -184,7 +188,7 @@ class ReplayWebApp extends LitElement
}
.info-menu {
padding: 0 1.0em;
padding: 0 1em;
}
.logo-text {
@@ -202,7 +206,6 @@ class ReplayWebApp extends LitElement
display: none !important;
}
}
`;
}
@@ -211,8 +214,10 @@ class ReplayWebApp extends LitElement
}
renderNavBrand() {
return html`
<span id="home" class="logo-text has-text-weight-bold is-size-6 has-allcaps wide-only">
return html` <span
id="home"
class="logo-text has-text-weight-bold is-size-6 has-allcaps wide-only"
>
<span class="has-text-primary">replay</span>
<span class="has-text-link">web.page</span>
<span class="is-sr-only">Home</span>
@@ -220,70 +225,145 @@ class ReplayWebApp extends LitElement
}
renderNavBar() {
return html`
<a href="#skip-main-target" @click=${this.skipMenu} class="skip-link">Skip main navigation</a>
return html` <a
href="#skip-main-target"
@click=${this.skipMenu}
class="skip-link"
>Skip main navigation</a
>
<nav class="navbar has-background-info" aria-label="main">
<div class="navbar-brand">
${!this.embed ? html`
<a href="${this.homeUrl}" class="navbar-item wr-logo-item" aria-labelledby="home">
<fa-icon id="wrlogo" size="1.0rem" .svg=${this.mainLogo} aria-hidden="true"></fa-icon>
${!this.embed
? html`
<a
href="${this.homeUrl}"
class="navbar-item wr-logo-item"
aria-labelledby="home"
>
<fa-icon
id="wrlogo"
size="1.0rem"
.svg=${this.mainLogo}
aria-hidden="true"
></fa-icon>
${this.renderNavBrand()}
</a>
${this.collTitle ? html`
<a href="${this.collPageUrl}" class="no-wrap is-size-6 has-text-black">/&nbsp;&nbsp;<i>${this.collTitle}</i></a>
<span class="no-wrap is-size-6">&nbsp;&nbsp;/&nbsp;
${this.pageReplay ? html`<i>${this.pageTitle}</i>` : this.pageTitle}
${this.collTitle
? html`
<a
href="${this.collPageUrl}"
class="no-wrap is-size-6 has-text-black"
>/&nbsp;&nbsp;<i>${this.collTitle}</i></a
>
<span class="no-wrap is-size-6"
>&nbsp;&nbsp;/&nbsp;
${this.pageReplay
? html`<i>${this.pageTitle}</i>`
: this.pageTitle}
</span>
` : ""}
`: html`
`
: ""}
`
: html`
<span class="navbar-item wr-logo-item">
<fa-icon id="wrlogo" size="1.0rem" .svg=${this.mainLogo} aria-hidden="true"></fa-icon>
<fa-icon
id="wrlogo"
size="1.0rem"
.svg=${this.mainLogo}
aria-hidden="true"
></fa-icon>
</span>
`}
<a href="#" role="button" id="menu-button" @click="${this.onNavMenu}" @keyup="${clickOnSpacebarPress}"
class="navbar-burger burger ${this.navMenuShown ? "is-active" : ""}" aria-label="main menu" aria-haspopup="true" aria-expanded="${this.navMenuShown}">
<a
href="#"
role="button"
id="menu-button"
@click="${this.onNavMenu}"
@keyup="${clickOnSpacebarPress}"
class="navbar-burger burger ${this.navMenuShown ? "is-active" : ""}"
aria-label="main menu"
aria-haspopup="true"
aria-expanded="${this.navMenuShown}"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
${!this.sourceUrl ? html`
<div class="navbar-menu ${this.navMenuShown ? "is-active" : ""}">
${!this.sourceUrl
? html` <div
class="navbar-menu ${this.navMenuShown ? "is-active" : ""}"
>
<div class="navbar-start">
${IS_APP ? html`
<a role="button" href="#" class="navbar-item arrow-button" title="Go Back" @click="${() => window.history.back()}" @keyup="${clickOnSpacebarPress}">
<fa-icon size="1.0rem" .svg="${fasArrowLeft}" aria-hidden="true"></fa-icon><span class="menu-only is-size-7">&nbsp;Go Back</span>
${IS_APP
? html`
<a
role="button"
href="#"
class="navbar-item arrow-button"
title="Go Back"
@click="${() => window.history.back()}"
@keyup="${clickOnSpacebarPress}"
>
<fa-icon
size="1.0rem"
.svg="${fasArrowLeft}"
aria-hidden="true"
></fa-icon
><span class="menu-only is-size-7">&nbsp;Go Back</span>
</a>
<a role="button" href="#" class="navbar-item arrow-button" title="Go Forward" @click="${() => window.history.forward()}" @keyup="${clickOnSpacebarPress}">
<fa-icon size="1.0rem" .svg="${fasArrowRight}" aria-hidden="true"></fa-icon><span class="menu-only is-size-7">&nbsp;Go Forward</span>
<a
role="button"
href="#"
class="navbar-item arrow-button"
title="Go Forward"
@click="${() => window.history.forward()}"
@keyup="${clickOnSpacebarPress}"
>
<fa-icon
size="1.0rem"
.svg="${fasArrowRight}"
aria-hidden="true"
></fa-icon
><span class="menu-only is-size-7"
>&nbsp;Go Forward</span
>
</a>
` : ""}
`
: ""}
</div>
${!this.embed ? html`
<div class="navbar-end">
${this.renderNavEnd()}
</div>` : html``}
</div>` : html``}
${!this.embed
? html` <div class="navbar-end">${this.renderNavEnd()}</div>`
: html``}
</div>`
: html``}
</nav>
<p id="skip-main-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
}
renderNavEnd() {
return html`
<a href="/docs" target="_blank" class="navbar-item is-size-6">
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon><span>&nbsp;User Docs</span>
return html` <a href="/docs" target="_blank" class="navbar-item is-size-6">
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon
><span>&nbsp;User Docs</span>
</a>
<!--
-- NB: the About modal is currently inaccessible to people using keyboards or screen readers.
-- Should all the JS and infrastructure for accessible modals be added, or should About be a normal page?
-->
<a href="?terms" @click="${(e) => { e.preventDefault(); this.showAbout = true;} }"class="navbar-item is-size-6">About
<a
href="?terms"
@click="${(e) => {
e.preventDefault();
this.showAbout = true;
}}"
class="navbar-item is-size-6"
>About
</a>`;
}
renderColl() {
return html`
<wr-coll .loadInfo="${this.loadInfo}"
return html` <wr-coll
.loadInfo="${this.loadInfo}"
sourceUrl="${this.sourceUrl}"
embed="${this.embed}"
appName="${this.appName}"
@@ -292,16 +372,29 @@ class ReplayWebApp extends LitElement
@replay-favicons=${this.onFavIcons}
@update-title=${this.onTitle}
@coll-loaded=${this.onCollLoaded}
@about-show=${() => this.showAbout = true}></wr-coll>`;
@about-show=${() => (this.showAbout = true)}
></wr-coll>`;
}
renderHomeIndex() {
return html`
<wr-coll-index>
${!IS_APP ? html`
<p slot="header" class="tagline is-size-5 has-text-centered">Explore and Replay Interactive Archived Webpages Directly in your Browser. <i><a target="_blank" href="./docs/examples">(See Examples)</a></i></p>
` : ""}
<wr-chooser slot="header" .droppedFile=${this.droppedFile} @did-drop-file="${() => this.droppedFile = null}" @load-start=${this.onStartLoad}></wr-chooser>
return html` <wr-coll-index>
${!IS_APP
? html`
<p slot="header" class="tagline is-size-5 has-text-centered">
Explore and Replay Interactive Archived Webpages Directly in your
Browser.
<i
><a target="_blank" href="./docs/examples">(See Examples)</a></i
>
</p>
`
: ""}
<wr-chooser
slot="header"
.droppedFile=${this.droppedFile}
@did-drop-file="${() => (this.droppedFile = null)}"
@load-start=${this.onStartLoad}
></wr-chooser>
</wr-coll-index>`;
}
@@ -316,11 +409,8 @@ class ReplayWebApp extends LitElement
return html`
${!this.embed || this.embed === "full" ? this.renderNavBar() : ""}
${this.sourceUrl ? this.renderColl() : this.renderHomeIndex()}
${this.showAbout ? this.renderAbout() : ""}
${this.showFileDropOverlay ? this.renderDropFileOverlay() : ""}
`;
}
@@ -332,7 +422,12 @@ class ReplayWebApp extends LitElement
const name = this.swName + (this.useRuffle ? "?ruffle=1" : "");
this.swmanager = new SWManager({ name, appName: this.appName });
this.swmanager.register().catch(() => this.swErrorMsg = this.swmanager.renderErrorReport(this.mainLogo));
this.swmanager
.register()
.catch(
() =>
(this.swErrorMsg = this.swmanager.renderErrorReport(this.mainLogo)),
);
window.addEventListener("popstate", () => {
this.initRoute();
@@ -377,18 +472,26 @@ class ReplayWebApp extends LitElement
// 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) => {
document.addEventListener(
"click",
(event) => {
event.preventDefault();
this.navMenuShown = false;
this.renderRoot.querySelector("#menu-button").focus();
}, {once: true});
document.addEventListener("keypress", (event) => {
},
{ once: true },
);
document.addEventListener(
"keypress",
(event) => {
if (event.key == "Escape") {
event.preventDefault();
this.navMenuShown = false;
this.renderRoot.querySelector("#menu-button").focus();
}
}, {once: true});
},
{ once: true },
);
}
}
@@ -401,7 +504,11 @@ class ReplayWebApp extends LitElement
if (state) {
try {
state = JSON.parse(state);
if ((state.ids instanceof Array) && state.userId && state.action === "open") {
if (
state.ids instanceof Array &&
state.userId &&
state.action === "open"
) {
this.pageParams.set("source", "googledrive://" + state.ids[0]);
this.pageParams.delete("state");
window.location.search = this.pageParams.toString();
@@ -448,7 +555,9 @@ class ReplayWebApp extends LitElement
if (this.pageParams.get("baseUrlSourcePrefix")) {
this.loadInfo.extraConfig = this.loadInfo.extraConfig || {};
this.loadInfo.extraConfig.baseUrlSourcePrefix = this.pageParams.get("baseUrlSourcePrefix");
this.loadInfo.extraConfig.baseUrlSourcePrefix = this.pageParams.get(
"baseUrlSourcePrefix",
);
}
if (this.pageParams.get("basePageUrl")) {
@@ -464,7 +573,6 @@ class ReplayWebApp extends LitElement
this.loadInfo.noWebWorker = true;
}
if (this.pageParams.get("noCache") === "1") {
this.loadInfo.noCache = true;
}
@@ -484,7 +592,7 @@ class ReplayWebApp extends LitElement
if (IS_APP && this.sourceUrl.startsWith("file://")) {
this.loadInfo = {
sourceUrl: this.sourceUrl,
loadUrl: this.sourceUrl.replace("file://", "file2://")
loadUrl: this.sourceUrl.replace("file://", "file2://"),
};
}
}
@@ -501,7 +609,6 @@ class ReplayWebApp extends LitElement
window.location.search = this.pageParams.toString();
return;
} else {
window.history.pushState({}, "", this.collPageUrl);
}
@@ -533,7 +640,11 @@ class ReplayWebApp extends LitElement
this.pageTitle = event.detail.title;
this.pageReplay = event.detail.replayTitle;
document.title = (event.detail.replayTitle ? "Archive of " : "") + this.pageTitle + " | " + this.appName;
document.title =
(event.detail.replayTitle ? "Archive of " : "") +
this.pageTitle +
" | " +
this.appName;
}
}
@@ -561,8 +672,12 @@ class ReplayWebApp extends LitElement
<div class="modal-background" @click="${this.onAboutClose}"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">About ReplayWeb.page ${IS_APP ? "App" : ""}</p>
<button class="delete" aria-label="close" @click="${this.onAboutClose}"></button>
<p class="modal-card-title">About ReplayWeb.page ${
IS_APP ? "App" : ""
}</p>
<button class="delete" aria-label="close" @click="${
this.onAboutClose
}"></button>
</header>
<section class="modal-card-body">
<div class="container">
@@ -570,16 +685,29 @@ class ReplayWebApp extends LitElement
<div style="display: flex">
<div class="has-text-centered" style="width: 220px">
<wr-anim-logo class="logo" size="48px"></wr-anim-logo>
<div style="font-size: smaller; margin-bottom: 1em">${IS_APP ? "App" : ""} v${VERSION}</div>
<div style="font-size: smaller; margin-bottom: 1em">${
IS_APP ? "App" : ""
} v${VERSION}</div>
</div>
${IS_APP ? html`
<p>ReplayWeb.page App is a standalone app for Mac, Windows and Linux that loads web archive files provided by the user
and renders them for replay.</p>
` : html`
<p><a href="https://replayweb.page" target="_blank">ReplayWeb.page</a> is a browser-based viewer that loads web archive files provided by the user
and renders them for replay in the browser.</p>`}
${
IS_APP
? html`
<p>
ReplayWeb.page App is a standalone app for Mac,
Windows and Linux that loads web archive files
provided by the user and renders them for replay.
</p>
`
: html` <p>
<a href="https://replayweb.page" target="_blank"
>ReplayWeb.page</a
>
is a browser-based viewer that loads web archive
files provided by the user and renders them for
replay in the browser.
</p>`
}
</div>
<p>Full source code is available at:
@@ -604,7 +732,9 @@ class ReplayWebApp extends LitElement
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
</details>
<div class="has-text-centered">
<a class="button is-warning" href="#" @click="${this.onAboutClose}">Close</a>
<a class="button is-warning" href="#" @click="${
this.onAboutClose
}">Close</a>
</div>
</div>
</div>
+79 -34
View File
@@ -3,10 +3,8 @@ import { IS_APP, wrapCss } from "./misc";
import fasUpload from "@fortawesome/fontawesome-free/svgs/solid/upload.svg";
// ===========================================================================
class Chooser extends LitElement
{
class Chooser extends LitElement {
constructor() {
super();
@@ -30,9 +28,9 @@ class Chooser extends LitElement
"application/wacz": [".wacz"],
"application/wbn": [".wbn"],
"application/json": [".json"],
}
}
]
},
},
],
};
}
@@ -41,7 +39,7 @@ class Chooser extends LitElement
fileDisplayName: { type: String },
droppedFile: { type: File },
newFullImport: { type: Boolean },
noHead: { type: Boolean }
noHead: { type: Boolean },
};
}
@@ -52,13 +50,20 @@ class Chooser extends LitElement
}
onDropFile() {
const allowedFileExtensions = this.showOpenFilePickerOptions.types.map(type => type.accept).map(Object.values).flat(2);
const allowedFileExtensions = this.showOpenFilePickerOptions.types
.map((type) => type.accept)
.map(Object.values)
.flat(2);
const fileHasAllowedExtension = allowedFileExtensions.some(extension => this.droppedFile.name.endsWith(extension));
const fileHasAllowedExtension = allowedFileExtensions.some((extension) =>
this.droppedFile.name.endsWith(extension),
);
if (fileHasAllowedExtension) {
this.setFile(this.droppedFile);
this.dispatchEvent(new CustomEvent("did-drop-file", { bubbles: true, composed: true }));
this.dispatchEvent(
new CustomEvent("did-drop-file", { bubbles: true, composed: true }),
);
this.onStartLoad(); // Automatically load the file
}
}
@@ -81,7 +86,9 @@ class Chooser extends LitElement
return;
}
const [fileHandle] = await window.showOpenFilePicker(this.showOpenFilePickerOptions);
const [fileHandle] = await window.showOpenFilePicker(
this.showOpenFilePickerOptions,
);
this.fileHandle = fileHandle;
this.file = await fileHandle.getFile();
@@ -89,7 +96,10 @@ class Chooser extends LitElement
}
randomId() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
return (
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15)
);
}
onStartLoad(event) {
@@ -121,7 +131,13 @@ class Chooser extends LitElement
loadInfo.newFullImport = this.newFullImport;
this.dispatchEvent(new CustomEvent("load-start", {bubbles: true, composed: true, detail: loadInfo}));
this.dispatchEvent(
new CustomEvent("load-start", {
bubbles: true,
composed: true,
detail: loadInfo,
}),
);
return false;
}
@@ -129,7 +145,11 @@ class Chooser extends LitElement
onInput(event) {
this.fileDisplayName = event.currentTarget.value;
if (this.file && this.fileDisplayName && this.fileDisplayName.startsWith("file://")) {
if (
this.file &&
this.fileDisplayName &&
this.fileDisplayName.startsWith("file://")
) {
this.file = null;
this.fileDisplayName = "";
}
@@ -144,8 +164,8 @@ class Chooser extends LitElement
padding: 1.5em;
}
.less-padding {
padding-top: 1.0em;
padding-bottom: 1.0em;
padding-top: 1em;
padding-bottom: 1em;
}
div.field.has-addons {
flex: auto;
@@ -192,42 +212,67 @@ class Chooser extends LitElement
}
render() {
return html`
<section class="section ${this.noHead ? "is-paddingless" : "less-padding"}">
return html` <section
class="section ${this.noHead ? "is-paddingless" : "less-padding"}"
>
<div class="${this.noHead ? "" : "panel"}">
<div class="${this.noHead ? "is-hidden" : "panel-heading"} heading-size">${this.newFullImport ? "Import Existing" : "Load"} Web Archive</div>
<div class="${this.noHead ? "" : "panel-body extra-padding"} file has-name">
<div
class="${this.noHead ? "is-hidden" : "panel-heading"} heading-size"
>
${this.newFullImport ? "Import Existing" : "Load"} Web Archive
</div>
<div
class="${this.noHead ? "" : "panel-body extra-padding"} file has-name"
>
<form class="is-flex" @submit="${this.onStartLoad}">
<label class="file-label">
${!this.hasNativeFS ? html`
<input class="file-input"
@click="${(e) => e.currentTarget.value = null}"
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">` : ""}
${!this.hasNativeFS
? html` <input
class="file-input"
@click="${(e) => (e.currentTarget.value = null)}"
@change=${this.onChooseFile}
type="file"
id="fileupload"
name="fileupload"
/>`
: ""}
<span class="file-cta" @click="${this.onChooseNativeFile}">
<span class="file-icon">
<fa-icon size="0.9em" .svg=${fasUpload} aria-hidden="true"></fa-icon>
</span>
<span class="file-label is-hidden-touch">
Choose File...
<fa-icon
size="0.9em"
.svg=${fasUpload}
aria-hidden="true"
></fa-icon>
</span>
<span class="file-label is-hidden-touch"> Choose File... </span>
</span>
</label>
<div class="field has-addons">
<p class="control is-expanded">
<input class="file-name input" type="text"
name="filename" id="filename"
pattern="((file|http|https|ipfs|s3):\/\/.*\.(warc|warc.gz|zip|wacz|har|wbn|json)([?#].*)?)|(googledrive:\/\/.+)|(ssb:\/\/.+)"
<input
class="file-name input"
type="text"
name="filename"
id="filename"
pattern="((file|http|https|ipfs|s3)://.*.(warc|warc.gz|zip|wacz|har|wbn|json)([?#].*)?)|(googledrive://.+)|(ssb://.+)"
.value="${this.fileDisplayName}"
@input="${this.onInput}"
autocomplete="off"
placeholder="${this.newFullImport ? "Click 'Choose File' to select a local archive to import" : "Enter a URL or click 'Choose File' to select a WARC, WACZ, HAR or WBN archive source"}">
placeholder="${this.newFullImport
? "Click 'Choose File' to select a local archive to import"
: "Enter a URL or click 'Choose File' to select a WARC, WACZ, HAR or WBN archive source"}"
/>
</p>
<div class="control">
<button type="submit" class="button is-hidden-mobile is-primary">${this.newFullImport ? "Import" : "Load"}</button>
<button
type="submit"
class="button is-hidden-mobile is-primary"
>
${this.newFullImport ? "Import" : "Load"}
</button>
</div>
</div>
</form>
</div>
</div>
+140 -73
View File
@@ -10,10 +10,8 @@ import fasArrowDown from "@fortawesome/fontawesome-free/svgs/solid/angle-double-
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
// ===========================================================================
class CollIndex extends LitElement
{
class CollIndex extends LitElement {
constructor() {
super();
@@ -36,17 +34,13 @@ class CollIndex extends LitElement
get sortKeys() {
return [
{key: "title",
name: "Title"},
{ key: "title", name: "Title" },
{key: "sourceUrl",
name: "Source"},
{ key: "sourceUrl", name: "Source" },
{key: "ctime",
name: this.dateName},
{ key: "ctime", name: this.dateName },
{key: "size",
name: "Total Size"}
{ key: "size", name: "Total Size" },
];
}
@@ -91,10 +85,12 @@ class CollIndex extends LitElement
this.filteredColls = [];
for (const coll of this.colls) {
if (coll.sourceUrl.indexOf(this.query) >= 0 ||
if (
coll.sourceUrl.indexOf(this.query) >= 0 ||
coll.filename.indexOf(this.query) >= 0 ||
(coll.loadUrl && coll.loadUrl.indexOf(this.query) >= 0) ||
(coll.title && coll.title.indexOf(this.query) >= 0)) {
(coll.title && coll.title.indexOf(this.query) >= 0)
) {
this.filteredColls.push(coll);
}
}
@@ -114,7 +110,6 @@ class CollIndex extends LitElement
this._deleting = {};
this.sortedColls = [];
} catch (e) {
// likely no sw registered yet, or waiting for new sw to register, retry again
setTimeout(() => this.loadColls(), 500);
@@ -165,7 +160,7 @@ class CollIndex extends LitElement
padding: 2em;
}
.no-top-padding {
padding-top: 1.0em;
padding-top: 1em;
}
.panel-heading {
font-size: 0.85rem;
@@ -253,7 +248,9 @@ class CollIndex extends LitElement
}
renderHeader() {
return html`<h2 class="panel-heading panel-color"><span>${this.headerName}</span></h2>`;
return html`<h2 class="panel-heading panel-color">
<span>${this.headerName}</span>
</h2>`;
}
renderSearchHeader() {
@@ -269,48 +266,79 @@ class CollIndex extends LitElement
</header>
<section class="section no-top-padding">
<div class="sort-header is-small">
${hasHeader ? html`
<button @click=${() => this.hideHeader = !this.hideHeader} class="collapse button is-small">
<span class="icon"><fa-icon .svg=${this.hideHeader ? fasArrowDown : fasArrowUp}></span>
<span>${this.hideHeader ? "Show " : "Hide"} <span class="is-sr-only">Header</span></span>
</button>` : ""}
${hasHeader
? html`
<button @click=${() =>
(this.hideHeader =
!this.hideHeader)} class="collapse button is-small">
<span class="icon"><fa-icon .svg=${
this.hideHeader ? fasArrowDown : fasArrowUp
}></span>
<span>${
this.hideHeader ? "Show " : "Hide"
} <span class="is-sr-only">Header</span></span>
</button>`
: ""}
</div>
<div class="panel">
${this.renderHeader()}
${this.colls.length ? html`
${this.colls.length
? html`
<div class="panel-block sort-header is-small">
${this.renderSearchHeader()}
<div class="control has-icons-left has-addons">
<input type="text" class="input is-small" @input="${(e) => this.query = e.currentTarget.value}" .value="${this.query}" type="text"
placeholder="Search by Archive Title or Source">
<span class="icon is-left is-small"><fa-icon .svg="${fasSearch}"/></span>
<input
type="text"
class="input is-small"
@input="${(e) => (this.query = e.currentTarget.value)}"
.value="${this.query}"
type="text"
placeholder="Search by Archive Title or Source"
/>
<span class="icon is-left is-small"
><fa-icon .svg="${fasSearch}"
/></span>
</div>
<wr-sorter id="index"
<wr-sorter
id="index"
sortKey="ctime"
?sortDesc="${true}"
.sortKeys="${this.sortKeys}"
.data="${this.filteredColls}"
@sort-changed="${(e) => this.sortedColls = e.detail.sortedData}">
@sort-changed="${(e) =>
(this.sortedColls = e.detail.sortedData)}"
>
</wr-sorter>
</div>
<div class="coll-list">
${this.sortedColls && this.sortedColls.map((coll, i) => html`
${this.sortedColls &&
this.sortedColls.map(
(coll, i) => html`
<div class="coll-block panel-block">
${this.renderCollInfo(coll)}
${!this._deleting[coll.sourceUrl] ? html`
<button class="delete delete-button" aria-label="Unload Collection" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
` : html`
${!this._deleting[coll.sourceUrl]
? html`
<button
class="delete delete-button"
aria-label="Unload Collection"
title="Unload Collection"
data-coll-index="${i}"
@click="${this.onDeleteColl}"
></button>
`
: html`
<span class="button delete-button is-loading is-static">Deleting</span`}
</div>
`)}
`,
)}
</div>
` : html`
`
: html`
<div class="panel-block extra-padding">
${this.sortedColls === null ? html`<i>Loading Archives...</i>` :
this.renderEmpty()}
${this.sortedColls === null
? html`<i>Loading Archives...</i>`
: this.renderEmpty()}
</div>
`}
</div>
@@ -323,14 +351,15 @@ class CollIndex extends LitElement
}
renderEmpty() {
return html`<i>No Archives so far! Archives loaded in the section above will appear here.</i>`;
return html`<i
>No Archives so far! Archives loaded in the section above will appear
here.</i
>`;
}
}
// ===========================================================================
class CollInfo extends LitElement
{
class CollInfo extends LitElement {
constructor() {
super();
this.detailed = false;
@@ -341,7 +370,7 @@ class CollInfo extends LitElement
return {
coll: { type: Object },
detailed: { type: Boolean },
canDelete: { type: Boolean }
canDelete: { type: Boolean },
};
}
@@ -375,13 +404,14 @@ class CollInfo extends LitElement
}
.col-title:hover {
}
.col-title a {
display: block;
height: 100%;
}
.column:hover > .copy, .source-text:hover + .copy, .copy:hover {
.column:hover > .copy,
.source-text:hover + .copy,
.copy:hover {
display: inline;
}
.copy {
@@ -407,29 +437,42 @@ class CollInfo extends LitElement
renderSource(coll) {
return html`
<div class="column is-4">
<span class="source-text"><p class="minihead">Source</p>
${coll.sourceUrl && (coll.sourceUrl.startsWith("http://") || coll.sourceUrl.startsWith("https://")) ? html`
<a href="${coll.sourceUrl}">${coll.sourceUrl}&nbsp;</a>` : html`
${coll.sourceUrl}&nbsp;`}
<span class="source-text"
><p class="minihead">Source</p>
${coll.sourceUrl &&
(coll.sourceUrl.startsWith("http://") ||
coll.sourceUrl.startsWith("https://"))
? html` <a href="${coll.sourceUrl}">${coll.sourceUrl}&nbsp;</a>`
: html` ${coll.sourceUrl}&nbsp;`}
</span>
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"><fa-icon .svg="${fasCopy}"/></a>
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://") ? html`
<p><i>(${coll.filename})</i></p>` : ""}
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"
><fa-icon .svg="${fasCopy}"
/></a>
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://")
? html` <p><i>(${coll.filename})</i></p>`
: ""}
</div>
<div class="column is-2">
<p class="minihead">Date Loaded</p>
${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}
</div>
<div class="column is-2">
<p class="minihead">Total Size</p>
${prettyBytes(Number(coll.size || 0))}
</div>
<div class="column is-2"><p class="minihead">Date Loaded</p>${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</div>
<div class="column is-2"><p class="minihead">Total Size</p>${prettyBytes(Number(coll.size || 0))}</div>
`;
}
renderSummaryView() {
const coll = this.coll;
return html`
<div class="columns">
return html` <div class="columns">
<div class="column col-title is-4">
<span class="subtitle has-text-weight-bold">
<a href="?source=${encodeURIComponent(coll.sourceUrl)}">${coll.title || coll.filename}</a>
<a href="?source=${encodeURIComponent(coll.sourceUrl)}"
>${coll.title || coll.filename}</a
>
</span>
</div>
${this.renderSource(coll)}
@@ -439,46 +482,70 @@ class CollInfo extends LitElement
renderDetailed() {
const coll = this.coll;
let {numValid, numInvalid, domain, certFingerprint, datapackageHash, publicKey, software} = this.coll.verify || {};
let {
numValid,
numInvalid,
domain,
certFingerprint,
datapackageHash,
publicKey,
software,
} = this.coll.verify || {};
numValid = numValid || 0;
numInvalid = numInvalid || 0;
const certFingerprintUrl = certFingerprint ? `https://crt.sh/?q=${certFingerprint}` : "";
const certFingerprintUrl = certFingerprint
? `https://crt.sh/?q=${certFingerprint}`
: "";
return html`
<div class="columns">
return html` <div class="columns">
<div class="column col-title is-4">
<span class="subtitle has-text-weight-bold">
${coll.title || coll.filename}
</span>
</div>
${coll.desc ? html`
<div class="column">
${coll.desc
? html` <div class="column">
<p class="minihead">Description</p>
${coll.desc}
</div>` : html`
`}
<div class="column"><p class="minihead">Filename</p>${coll.filename}</div>
</div>`
: html``}
<div class="column">
<p class="minihead">Filename</p>
${coll.filename}
</div>
${this.renderSource(coll)}
${domain ? html`
${domain
? html`
<div class="column">
<p class="minihead">Observed By</p>
<p>${domain}</p>
${certFingerprintUrl ? html`<span><a target="_blank" href="${certFingerprintUrl}">View Certificate</a></span>` : ""}
${certFingerprintUrl
? html`<span
><a target="_blank" href="${certFingerprintUrl}"
>View Certificate</a
></span
>`
: ""}
</div>
` : software ? html`
`
: software
? html`
<div class="column">
<p class="minihead">Created With</p>
${software || "Unknown"}
</div>
` : ""}
`
: ""}
<div class="column">
<p class="minihead">Validation</p>
${numValid > 0 || numInvalid > 0 ? html`
<p>${numValid} hashes verified${numInvalid ? html`, ${numInvalid} invalid` : ""}</p>` : html`
Not Available`}
${numValid > 0 || numInvalid > 0
? html` <p>
${numValid} hashes
verified${numInvalid ? html`, ${numInvalid} invalid` : ""}
</p>`
: html` Not Available`}
</div>
<div class="column">
+553 -164
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -3,11 +3,10 @@
import { ElectronReplayApp } from "./electron-replay-app";
import path from "path";
// ============================================================================
const replayApp = new ElectronReplayApp({
staticPath: path.join(__dirname, "../"),
profileName: "replaywebpage"
profileName: "replaywebpage",
});
replayApp.init(true);
+1 -5
View File
@@ -4,7 +4,7 @@ import { CollectionLoader } from "@webrecorder/wabac/src/loaders";
const { ipcRenderer, contextBridge } = require("electron");
contextBridge.exposeInMainWorld("electron", {"IS_APP": true});
contextBridge.exposeInMainWorld("electron", { IS_APP: true });
const dbs = {};
@@ -46,7 +46,3 @@ async function getResponse(event, request, coll, ts, channel) {
ipcRenderer.on("getresponse", getResponse);
export { getDB, getColl, loader };
+79 -25
View File
@@ -3,7 +3,15 @@
import fetch from "node-fetch";
import { Headers } from "node-fetch";
import {app, session, BrowserWindow, ipcMain, protocol, screen, shell } from "electron";
import {
app,
session,
BrowserWindow,
ipcMain,
protocol,
screen,
shell,
} from "electron";
import path from "path";
import fs from "fs";
@@ -30,8 +38,7 @@ const FILE_PROTO = "file2";
const URL_RX = /([^/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
// ============================================================================
class ElectronReplayApp
{
class ElectronReplayApp {
constructor({ staticPath = "./", profileName = "" } = {}) {
this.pluginPath = "";
@@ -76,7 +83,9 @@ class ElectronReplayApp
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log("App already running, opening new window in first instance and quitting");
console.log(
"App already running, opening new window in first instance and quitting",
);
app.quit();
} else {
app.on("second-instance", (event, commandLine /*, workingDir*/) => {
@@ -93,10 +102,14 @@ class ElectronReplayApp
console.log("user data", app.getPath("userData"));
if (this.profileName) {
app.setPath("userData", path.join(app.getPath("appData"), this.profileName));
app.setPath(
"userData",
path.join(app.getPath("appData"), this.profileName),
);
}
protocol.registerSchemesAsPrivileged([{
protocol.registerSchemesAsPrivileged([
{
scheme: FILE_PROTO,
privileges: {
standard: false,
@@ -105,9 +118,10 @@ class ElectronReplayApp
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true
}
}]);
stream: true,
},
},
]);
app.on("will-finish-launching", () => {
app.on("open-file", (event, filePath) => {
@@ -168,9 +182,13 @@ class ElectronReplayApp
const sesh = session.defaultSession;
sesh.protocol.interceptStreamProtocol("http", (request, callback) => this.doIntercept(request, callback));
sesh.protocol.interceptStreamProtocol("http", (request, callback) =>
this.doIntercept(request, callback),
);
protocol.registerStreamProtocol(FILE_PROTO, (request, callback) => this.doHandleFile(request, callback));
protocol.registerStreamProtocol(FILE_PROTO, (request, callback) =>
this.doHandleFile(request, callback),
);
this.origUA = sesh.getUserAgent();
@@ -200,13 +218,19 @@ class ElectronReplayApp
const size = stat.size;
const {statusCode, start, end} = this.parseRange(reqHeaders, headers, size);
const { statusCode, start, end } = this.parseRange(
reqHeaders,
headers,
size,
);
const data = request.method === "HEAD" ? null : fs.createReadStream(filename, {start, end});
const data =
request.method === "HEAD"
? null
: fs.createReadStream(filename, { start, end });
callback({ statusCode, headers, data });
return;
} else {
return this.notFound("No Resource Specified", callback);
}
@@ -235,7 +259,6 @@ class ElectronReplayApp
return await this.resolveArchiveResponse(request, callback);
}
} else {
// try serve static file from app dir
let filename = request.url.slice(STATIC_PREFIX.length).split("?", 1)[0];
filename = filename.split("#", 1)[0];
@@ -261,7 +284,11 @@ class ElectronReplayApp
const data = fs.createReadStream(fullPath);
return callback({statusCode: 200, headers: {"content-type": mimeType}, data});
return callback({
statusCode: 200,
headers: { "content-type": mimeType },
data,
});
}
}
@@ -280,7 +307,9 @@ class ElectronReplayApp
let headers = request.headers;
const { method, url, uploadData } = request;
const body = uploadData ? Readable.from(readBody(uploadData, session.defaultSession)) : null;
const body = uploadData
? Readable.from(readBody(uploadData, session.defaultSession))
: null;
if (this.origUA) {
// pass UA if origUA is set
@@ -305,8 +334,14 @@ class ElectronReplayApp
notFound(url, callback) {
console.log("not found: " + url);
const data = this._bufferToStream(`Sorry, the url <b>${url}</b> could not be found in this archive.`);
callback({statusCode: 404, headers: {"Content-Type": "text/html; charset=\"utf-8\""}, data});
const data = this._bufferToStream(
`Sorry, the url <b>${url}</b> could not be found in this archive.`,
);
callback({
statusCode: 404,
headers: { "Content-Type": 'text/html; charset="utf-8"' },
data,
});
}
async resolveArchiveResponse(request, callback) {
@@ -324,7 +359,13 @@ class ElectronReplayApp
headers = new Headers(headers);
const date = new Date();
let response = new ArchiveResponse({payload, headers, status, date, url});
let response = new ArchiveResponse({
payload,
headers,
status,
date,
url,
});
const rewriter = new Rewriter({
baseUrl: url,
@@ -332,7 +373,7 @@ class ElectronReplayApp
urlRewrite: false,
contentRewrite: true,
decode: true,
useBaseRules: true
useBaseRules: true,
});
request.headers = new Headers(request.headers);
@@ -345,7 +386,11 @@ class ElectronReplayApp
let data = await response.getBuffer();
if (status === 206 || status === 200) {
const {statusCode, start, end} = this.parseRange(request.headers, headers, data.length);
const { statusCode, start, end } = this.parseRange(
request.headers,
headers,
data.length,
);
if (start !== undefined) {
data = data.slice(start, end);
}
@@ -358,11 +403,16 @@ class ElectronReplayApp
} catch (e) {
console.warn(e);
}
});
if (this.mainWindow) {
this.mainWindow.webContents.send("getresponse", request, this.proxyColl, this.proxyTS, channel);
this.mainWindow.webContents.send(
"getresponse",
request,
this.proxyColl,
this.proxyTS,
channel,
);
}
}
@@ -418,7 +468,11 @@ class ElectronReplayApp
getOpenUrl(argv) {
argv = require("minimist")(argv.slice(process.defaultApp ? 2 : 1));
const filename = this.openNextFile || argv.filename || argv.f || (argv._.length && argv._[0]);
const filename =
this.openNextFile ||
argv.filename ||
argv.f ||
(argv._.length && argv._[0]);
this.openNextFile = null;
let sourceString = "";
+62 -32
View File
@@ -1,4 +1,3 @@
import btGlobe from "../assets/globe.svg";
import btAngleDoubleDown from "../assets/chevron-double-down.svg";
import btAngleDoubleUp from "../assets/chevron-double-up.svg";
@@ -11,10 +10,8 @@ import { LitElement, html, css } from "lit";
import { tsToDate } from "./pageutils";
import prettyBytes from "pretty-bytes";
// ===========================================================================
export class RWPEmbedReceipt extends LitElement
{
export class RWPEmbedReceipt extends LitElement {
constructor() {
super();
this.collInfo = null;
@@ -35,7 +32,7 @@ export class RWPEmbedReceipt extends LitElement
url: { type: String },
ts: { type: String },
active: {type: Boolean }
active: { type: Boolean },
};
}
@@ -57,7 +54,7 @@ export class RWPEmbedReceipt extends LitElement
display: block;
z-index: 1;
pointer-events: none;
transition: all .3s linear;
transition: all 0.3s linear;
transform-origin: left top;
transform: scaleY(0);
transition: all 300ms cubic-bezier(0.15, 0, 0.1, 1);
@@ -81,7 +78,7 @@ export class RWPEmbedReceipt extends LitElement
max-width: 40rem;
width: calc(100% - 1rem);
height: 42px;
border-color: #D1D5DA;
border-color: #d1d5da;
border-width: 1px;
border-style: solid;
border-radius: 999px;
@@ -92,7 +89,7 @@ export class RWPEmbedReceipt extends LitElement
transition-duration: 50ms;
transition-timing-function: ease-out;
cursor: pointer;
z-index: 2
z-index: 2;
}
button.embed-info:active {
@@ -141,7 +138,7 @@ export class RWPEmbedReceipt extends LitElement
font-weight: bold;
text-transform: none;
letter-spacing: 0;
color: #24292E;
color: #24292e;
}
.embed-info-drop-statscontainer > h3 {
@@ -173,37 +170,52 @@ export class RWPEmbedReceipt extends LitElement
.embed-globe {
margin: 0.25rem;
padding: 7px;
background-color: #0366D6;
background-color: #0366d6;
border-radius: 9999px;
color: white;
border-width: 1px;
border-color: #D1D5DA;
border-color: #d1d5da;
border-style: solid;
line-height: 0.5em;
}`;
}
`;
}
render() {
let {numValid, numInvalid, domain, certFingerprint, datapackageHash, publicKey, software} = this.collInfo.verify || {};
let {
numValid,
numInvalid,
domain,
certFingerprint,
datapackageHash,
publicKey,
software,
} = this.collInfo.verify || {};
numValid = numValid || 0;
numInvalid = numInvalid || 0;
const sourceUrl = this.collInfo.sourceUrl;
const certFingerprintUrl = certFingerprint ? `https://crt.sh/?q=${certFingerprint}` : "";
const certFingerprintUrl = certFingerprint
? `https://crt.sh/?q=${certFingerprint}`
: "";
const dateStr = tsToDate(this.ts).toLocaleString();
return html`
<div class="dropdown mb-4 ${this.active ? "is-active" : ""}">
<div class="dropdown-trigger embed-info-container">
<button class="embed-info is-small is-rounded mt-4" aria-haspopup="true" aria-controls="embed-dropdown" @click="${this.onEmbedDrop}">
<button class="embed-info is-small is-rounded mt-4" aria-haspopup="true" aria-controls="embed-dropdown" @click="${
this.onEmbedDrop
}">
<fa-icon class="menu-logo mr-2 embed-globe" size="1rem" aria-hidden="true" .svg=${btGlobe}></fa-icon>
<span class="embed-info-buttontext">
This embed is part of a web archive. Click here to learn more.
</span>
<span class="icon is-small mr-4 ml-2">
<fa-icon title="Toggle" .svg="${this.active ? btAngleDoubleUp : btAngleDoubleDown}" aria-hidden="true"></fa-icon>
<fa-icon title="Toggle" .svg="${
this.active ? btAngleDoubleUp : btAngleDoubleDown
}" aria-hidden="true"></fa-icon>
</span>
</button>
</div>
@@ -228,33 +240,51 @@ export class RWPEmbedReceipt extends LitElement
<p><a target="_blank" href="${this.url}">${this.url}</a></p>
<h3 class="mt-2">Archived On:</h3>
<p>${dateStr}</p>
${domain ? html`
${
domain
? html`
<h3 class="mt-2">Observed By:</h3>
<p>${domain}</p>
${certFingerprintUrl ? html`
<p><a target="_blank" href="${certFingerprintUrl}">View Certificate</a></p>` : ""}
` : software ? html`
<h3 class="mt-2">Created With:</h3>
<p>${software}</p>` : ""}
${!domain && publicKey ? html`
<h3 class="mt-2">Observer Public Key:</h3>
<p class="show-key">${publicKey}</p>` : ""}
${certFingerprintUrl
? html` <p>
<a target="_blank" href="${certFingerprintUrl}"
>View Certificate</a
>
</p>`
: ""}
`
: software
? html` <h3 class="mt-2">Created With:</h3>
<p>${software}</p>`
: ""
}
${
!domain && publicKey
? html` <h3 class="mt-2">Observer Public Key:</h3>
<p class="show-key">${publicKey}</p>`
: ""
}
<h3 class="mt-2">Validation:</h3>
${numValid > 0 || numInvalid > 0 ? html`
<p>${numValid} hashes verified${numInvalid ? html`, ${numInvalid} invalid` : ""}</p>` : html`
<p>Not Available</p>
`}
${
numValid > 0 || numInvalid > 0
? html` <p>
${numValid} hashes
verified${numInvalid ? html`, ${numInvalid} invalid` : ""}
</p>`
: html` <p>Not Available</p> `
}
<h3 class="mt-2">Package Hash:</h3>
<p class="show-hash">${datapackageHash}</p>
<h3 class="mt-2">Size</h3>
<p>${prettyBytes(Number(this.collInfo.size || 0))}</p>
</div>
${sourceUrl ? html`
` : ""}
${sourceUrl ? html`` : ""}
<p class="is-size-7 is-flex is-justify-content-space-between" style="margin-top: 40px">
<span>
<a class="has-text-black" target="_blank" href="https://github.com/webrecorder/replayweb.page">
<fa-icon class="menu-logo mr-1" size="1.0rem" aria-hidden="true" .svg=${this.appLogo}></fa-icon>
<fa-icon class="menu-logo mr-1" size="1.0rem" aria-hidden="true" .svg=${
this.appLogo
}></fa-icon>
Powered by ReplayWeb.page
</a>
</span>
+42 -22
View File
@@ -4,17 +4,14 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { wrapCss, rwpLogo } from "./misc";
import { SWManager } from "./swmanager";
var scriptSrc = document.currentScript && document.currentScript.src;
var defaultReplayFile = "";
const DEFAULT_REPLAY_BASE = "https://replayweb.page/";
// ===========================================================================
class Embed extends LitElement
{
class Embed extends LitElement {
constructor() {
super();
this.replaybase = "./replay/";
@@ -109,7 +106,12 @@ class Embed extends LitElement
const scope = this.replaybase;
const requireSubdomainIframe = this.requireSubdomainIframe;
this.swmanager = new SWManager({name, scope, requireSubdomainIframe, appName});
this.swmanager = new SWManager({
name,
scope,
requireSubdomainIframe,
appName,
});
try {
await this.swmanager.register();
@@ -144,7 +146,9 @@ class Embed extends LitElement
firstUpdated() {
if (this.noSandbox) {
console.warn("The noSandbox flag is deprecated. ReplayWeb.page does not add a sandbox by default. To enable sandboxing, use 'sandbox' flag instead. This may result in PDFs not loading and pages opening in new windows, but may be more secure in some situations");
console.warn(
"The noSandbox flag is deprecated. ReplayWeb.page does not add a sandbox by default. To enable sandboxing, use 'sandbox' flag instead. This may result in PDFs not loading and pages opening in new windows, but may be more secure in some situations",
);
}
this.doRegister();
@@ -163,7 +167,10 @@ class Embed extends LitElement
// https://lil.law.harvard.edu/blog/2022/09/15/opportunities-and-challenges-of-client-side-playback/
// likely safari < 16, don't use web workers due to issues with split storage state
if (window.GestureEvent !== undefined && window.SharedWorker === undefined ){
if (
window.GestureEvent !== undefined &&
window.SharedWorker === undefined
) {
this.noWebWorker = true;
}
@@ -204,13 +211,14 @@ class Embed extends LitElement
}
updated(changedProperties) {
if (changedProperties.has("url") ||
if (
changedProperties.has("url") ||
changedProperties.has("ts") ||
changedProperties.has("query") ||
changedProperties.has("view") ||
changedProperties.has("source") ||
changedProperties.has("src")) {
changedProperties.has("src")
) {
this.embed = this.embed || "default";
if (this.src) {
@@ -265,7 +273,7 @@ class Embed extends LitElement
url: this.url,
ts: this.ts,
query: this.query,
view: this.view
view: this.view,
}).toString();
}
}
@@ -300,17 +308,25 @@ class Embed extends LitElement
render() {
return html`
${this.paramString && this.hashString && this.inited ? html`
<iframe sandbox="${ifDefined(this.sandbox ?
"allow-downloads allow-modals allow-orientation-lock allow-pointer-lock\
${this.paramString && this.hashString && this.inited
? html`
<iframe
sandbox="${ifDefined(
this.sandbox
? "allow-downloads allow-modals allow-orientation-lock allow-pointer-lock\
allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts\
allow-same-origin allow-forms" : undefined)}"
@load="${this.onLoad}" src="${this.replaybase}${this.replayfile}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"
title="Replay of ${this.title ? `${this.title}:` :""} ${this.url}"></iframe>
` : html``}
allow-same-origin allow-forms"
: undefined,
)}"
@load="${this.onLoad}"
src="${this.replaybase}${this.replayfile}?${this
.paramString}#${this.hashString}"
allow="autoplay *; fullscreen"
title="Replay of ${this.title ? `${this.title}:` : ""} ${this
.url}"
></iframe>
`
: html``}
${this.errorMessage}
`;
}
@@ -323,7 +339,11 @@ class Embed extends LitElement
const win = event.target.contentWindow;
const doc = event.target.contentDocument;
if (win.navigator.serviceWorker && !win.navigator.serviceWorker.controller && this.reloadCount <= 2) {
if (
win.navigator.serviceWorker &&
!win.navigator.serviceWorker.controller &&
this.reloadCount <= 2
) {
this.reloadCount++;
setTimeout(() => win.location.reload(), 100);
return;
+49 -26
View File
@@ -3,10 +3,8 @@ import { wrapCss } from "./misc";
import fabGoogleDrive from "@fortawesome/fontawesome-free/svgs/brands/google-drive.svg";
// ===========================================================================
class GDrive extends LitElement
{
class GDrive extends LitElement {
constructor() {
super();
this.state = "trypublic";
@@ -20,7 +18,7 @@ class GDrive extends LitElement
state: { type: String },
sourceUrl: { type: String },
error: { type: Boolean },
reauth: { type: Boolean }
reauth: { type: Boolean },
};
}
@@ -78,9 +76,12 @@ class GDrive extends LitElement
const extra = { publicUrl };
const size = Number(json.size);
this.dispatchEvent(new CustomEvent("load-ready", {detail: {name, extra, size, sourceUrl}}));
this.dispatchEvent(
new CustomEvent("load-ready", {
detail: { name, extra, size, sourceUrl },
}),
);
return true;
} catch (e) {
return false;
}
@@ -112,11 +113,14 @@ class GDrive extends LitElement
const fileId = sourceUrl.slice("googledrive://".length);
const metadataUrl = `https://www.googleapis.com/drive/v3/files/${fileId}`;
const authToken = response.access_token;
const headers = {"Authorization": `Bearer ${authToken}`};
const headers = { Authorization: `Bearer ${authToken}` };
const resp = await fetch(metadataUrl + "?fields=name,size&supportsAllDrives=true", {headers});
const resp = await fetch(
metadataUrl + "?fields=name,size&supportsAllDrives=true",
{ headers },
);
if ((resp.status === 404 || resp.status == 403)) {
if (resp.status === 404 || resp.status == 403) {
if (this.state !== "implicitonly") {
this.state = "trymanual";
}
@@ -130,7 +134,11 @@ class GDrive extends LitElement
const name = metadata.name;
const size = Number(metadata.size);
this.dispatchEvent(new CustomEvent("load-ready", {detail: {sourceUrl, headers, size, name}}));
this.dispatchEvent(
new CustomEvent("load-ready", {
detail: { sourceUrl, headers, size, name },
}),
);
}
static get styles() {
@@ -138,21 +146,33 @@ class GDrive extends LitElement
}
render() {
return html`
${this.script()}
${this.state !== "trymanual" ? html`
<p>Connecting to Google Drive...</p>
` : html`
${this.error ? html`
return html` ${this.script()}
${this.state !== "trymanual"
? html` <p>Connecting to Google Drive...</p> `
: html`
${this.error
? html`
<div class="error has-text-danger">
<p>${this.reauth ? "Some resources are loaded on demand from Google Drive, which requires reauthorization." :
"Could not access this file with the current Google Drive account."}</p>
<p>If you have multiple Google Drive accounts, be sure to select the correct one.</p>
<p>
${this.reauth
? "Some resources are loaded on demand from Google Drive, which requires reauthorization."
: "Could not access this file with the current Google Drive account."}
</p>
<p>
If you have multiple Google Drive accounts, be sure to
select the correct one.
</p>
</div>
<br />
` : ""}
<button class="button is-warning is-rounded" @click="${this.onClickAuth}">
<span class="icon"><fa-icon .svg="${fabGoogleDrive}"></fa-icon></span>
`
: ""}
<button
class="button is-warning is-rounded"
@click="${this.onClickAuth}"
>
<span class="icon"
><fa-icon .svg="${fabGoogleDrive}"></fa-icon
></span>
<span>Authorize Google Drive</span>
</button>
`}`;
@@ -163,20 +183,23 @@ class GDrive extends LitElement
return html``;
}
const script = document.createElement("script");
script.onload = (() => this.onLoad());
script.onload = () => this.onLoad();
script.src = "https://apis.google.com/js/platform.js";
return script;
}
gauth(prompt, callback) {
self.gapi.load("auth2", () => {
self.gapi.auth2.authorize({
self.gapi.auth2.authorize(
{
// eslint-disable-next-line no-undef
client_id: __GDRIVE_CLIENT_ID__,
scope: "https://www.googleapis.com/auth/drive.file",
response_type: "token",
prompt
}, callback);
prompt,
},
callback,
);
});
}
}
+80 -43
View File
@@ -5,10 +5,8 @@ import prettyBytes from "pretty-bytes";
import { parseURLSchemeHostPath } from "./pageutils";
// ===========================================================================
class Loader extends LitElement
{
class Loader extends LitElement {
constructor() {
super();
this.progress = 0;
@@ -84,7 +82,10 @@ class Loader extends LitElement
this.fileHandle = event.data.fileHandle;
if (this.error === "missing_local_file") {
this.tryFileHandle = false;
} else if (this.error === "permission_needed" && event.data.fileHandle) {
} else if (
this.error === "permission_needed" &&
event.data.fileHandle
) {
this.state = "permission_needed";
break;
}
@@ -104,7 +105,9 @@ class Loader extends LitElement
}
this.progress = this.total;
this.percent = 100;
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: event.data}));
this.dispatchEvent(
new CustomEvent("coll-loaded", { detail: event.data }),
);
if (!this.noWebWorker) {
this.worker.terminate();
@@ -146,9 +149,11 @@ class Loader extends LitElement
break;
case "s3":
source = {sourceUrl,
source = {
sourceUrl,
loadUrl: `https://${host}.s3.amazonaws.com${path}`,
name: this.sourceUrl};
name: this.sourceUrl,
};
break;
case "file":
@@ -190,7 +195,11 @@ You can select a file to upload from the main page by clicking the 'Choose File.
extraConfig = this.loadInfo.extraConfig;
}
// todo: too special case?
if (sourceUrl.startsWith("proxy:") && extraConfig && extraConfig.recording) {
if (
sourceUrl.startsWith("proxy:") &&
extraConfig &&
extraConfig.recording
) {
type = "recordingproxy";
}
}
@@ -201,12 +210,14 @@ You can select a file to upload from the main page by clicking the 'Choose File.
extraConfig,
type,
skipExisting: true,
file: source
file: source,
};
if (!navigator.serviceWorker.controller) {
await new Promise((resolve) => {
navigator.serviceWorker.addEventListener("controllerchange", () => resolve());
navigator.serviceWorker.addEventListener("controllerchange", () =>
resolve(),
);
});
}
@@ -219,14 +230,14 @@ You can select a file to upload from the main page by clicking the 'Choose File.
// ping service worker with messages to avoid shutdown while loading
// (mostly for Firefox)
this.pingInterval = setInterval(() => {
navigator.serviceWorker.controller.postMessage({"msg_type": "ping"});
navigator.serviceWorker.controller.postMessage({ msg_type: "ping" });
}, 15000);
}
}
}
googledriveInit() {
this._gdWait = new Promise((resolve) => this._gdResolve = resolve);
this._gdWait = new Promise((resolve) => (this._gdResolve = resolve));
return this._gdWait;
}
@@ -244,7 +255,7 @@ You can select a file to upload from the main page by clicking the 'Choose File.
return;
}
const msg = {"msg_type": "cancelLoad", "name": this.coll};
const msg = { msg_type: "cancelLoad", name: this.coll };
if (!this.noWebWorker) {
this.worker.postMessage(msg);
@@ -260,7 +271,10 @@ You can select a file to upload from the main page by clicking the 'Choose File.
}
updated(changedProperties) {
if (this.sourceUrl && changedProperties.has("sourceUrl") || changedProperties.has("tryFileHandle")) {
if (
(this.sourceUrl && changedProperties.has("sourceUrl")) ||
changedProperties.has("tryFileHandle")
) {
this.doLoad();
}
}
@@ -318,10 +332,11 @@ You can select a file to upload from the main page by clicking the 'Choose File.
<div class="has-text-centered is-flex">
<wr-anim-logo class="logo" size="96px" />
</div>
${!this.embed ? html`
<div class="level">
${!this.embed
? html` <div class="level">
<p class="level-item">Loading&nbsp;<b>${this.sourceUrl}</b>...</p>
</div>` : ""}
</div>`
: ""}
<div class="level">
<div class="level-item has-text-centered">
${this.renderContent()}
@@ -334,48 +349,70 @@ You can select a file to upload from the main page by clicking the 'Choose File.
renderContent() {
switch (this.state) {
case "googledrive":
return html`<wr-gdrive .sourceUrl=${this.sourceUrl} @load-ready=${this.onLoadReady}/>`;
return html`<wr-gdrive
.sourceUrl=${this.sourceUrl}
@load-ready=${this.onLoadReady}
/>`;
case "started":
return html`
<div class="progress-div">
<progress id="progress" class="progress is-primary is-large"
value="${this.percent}" max="100"></progress>
return html` <div class="progress-div">
<progress
id="progress"
class="progress is-primary is-large"
value="${this.percent}"
max="100"
></progress>
<label class="progress-label" for="progress">${this.percent}%</label>
${this.currentSize && this.totalSize ? html`
<div class="loaded-prog">Loaded <b>${prettyBytes(this.currentSize)}</b> of <b>${prettyBytes(this.totalSize)}</b>
${this.extraMsg && html`
<p class="extra-msg">(${this.extraMsg})</p>
`}
</div>` : html``}
${!this.embed ? html`
<button @click="${this.onCancel}" class="button is-danger">Cancel</button>` : ""}
${this.currentSize && this.totalSize
? html` <div class="loaded-prog">
Loaded <b>${prettyBytes(this.currentSize)}</b> of
<b>${prettyBytes(this.totalSize)}</b>
${this.extraMsg &&
html` <p class="extra-msg">(${this.extraMsg})</p> `}
</div>`
: html``}
${!this.embed
? html` <button @click="${this.onCancel}" class="button is-danger">
Cancel
</button>`
: ""}
</div>`;
case "errored":
return html`
<div class="has-text-left">
return html` <div class="has-text-left">
<div class="error has-text-danger">${this.error}</div>
<div>
${this.errorAllowRetry ? html`
<a class="button is-warning" @click=${() => window.parent.location.reload()}>Try Again</a>` : ""}
${this.embed ? html`` : html`
<a href="/" class="button is-warning">Back</a>`}
${this.errorAllowRetry
? html` <a
class="button is-warning"
@click=${() => window.parent.location.reload()}
>Try Again</a
>`
: ""}
${this.embed
? html``
: html` <a href="/" class="button is-warning">Back</a>`}
</div>
</div>`;
case "permission_needed":
return html`
<div class="has-text-left">
<div class="">Permission is needed to reload the archive file. (Click <i>Cancel</i> to cancel loading this archive.)</div>
<button @click="${this.onAskPermission}" class="button is-primary">Show Permission</button>
return html` <div class="has-text-left">
<div class="">
Permission is needed to reload the archive file. (Click
<i>Cancel</i> to cancel loading this archive.)
</div>
<button @click="${this.onAskPermission}" class="button is-primary">
Show Permission
</button>
<a href="/" class="button is-danger">Cancel</a>
</div>`;
case "waiting":
default:
return html`<progress class="progress is-primary is-large" style="max-width: 400px"/>`;
return html`<progress
class="progress is-primary is-large"
style="max-width: 400px"
/>`;
}
}
+72 -33
View File
@@ -11,14 +11,16 @@ import rwpLogo from "../assets/logo.svg";
const apiPrefix = "./w/api";
const replayPrefix = "./w";
// ===========================================================================
const allCss = unsafeCSS(allCssRaw);
function wrapCss(custom) {
return [allCss, custom];
}
const IS_APP = window.IS_APP || window.electron && window.electron.IS_APP || window.matchMedia("(display-mode: standalone)").matches;
const IS_APP =
window.IS_APP ||
(window.electron && window.electron.IS_APP) ||
window.matchMedia("(display-mode: standalone)").matches;
// eslint-disable-next-line no-undef
const VERSION = __VERSION__;
@@ -33,10 +35,8 @@ function clickOnSpacebarPress(event) {
}
}
// ===========================================================================
class FaIcon extends LitElement
{
class FaIcon extends LitElement {
constructor() {
super();
this.size = "1.1em";
@@ -49,7 +49,7 @@ class FaIcon extends LitElement
svg: { type: String },
size: { type: String },
width: { type: String },
height: { type: String }
height: { type: String },
};
}
@@ -59,7 +59,7 @@ class FaIcon extends LitElement
display: inline-block;
padding: 0;
margin: 0;
line-height: 1.0em;
line-height: 1em;
}
:host svg {
fill: var(--fa-icon-fill-color, currentcolor);
@@ -88,14 +88,14 @@ class FaIcon extends LitElement
}
}
return html`<svg style="${styleMap(styles)}"><g>${unsafeSVG(this.svg)}</g></svg>`;
return html`<svg style="${styleMap(styles)}">
<g>${unsafeSVG(this.svg)}</g>
</svg>`;
}
}
// ===========================================================================
class AnimLogo extends FaIcon
{
class AnimLogo extends FaIcon {
constructor() {
super();
this.svg = rwpLogo;
@@ -112,28 +112,46 @@ class AnimLogo extends FaIcon
}
@keyframes animLeft {
0% {stop-color: #4876ff}
25% {stop-color: #1b0921}
50% {stop-color: #4876ff}
75% {stop-color: #04cdff}
100% {stop-color: #4876ff}
0% {
stop-color: #4876ff;
}
25% {
stop-color: #1b0921;
}
50% {
stop-color: #4876ff;
}
75% {
stop-color: #04cdff;
}
100% {
stop-color: #4876ff;
}
}
@keyframes animRight {
0% {stop-color: #04cdff}
25% {stop-color: #4876ff}
50% {stop-color: #1b0921}
75% {stop-color: #4876ff}
100% {stop-color: #04cdff}
0% {
stop-color: #04cdff;
}
25% {
stop-color: #4876ff;
}
50% {
stop-color: #1b0921;
}
75% {
stop-color: #4876ff;
}
100% {
stop-color: #04cdff;
}
}
`;
}
}
// ===========================================================================
class WrModal extends LitElement
{
class WrModal extends LitElement {
constructor() {
super();
this.title = "";
@@ -145,14 +163,14 @@ class WrModal extends LitElement
return {
title: { type: String },
bgClass: { type: String },
noBgClose: { type: Boolean }
noBgClose: { type: Boolean },
};
}
static get styles() {
return wrapCss(css`
.modal-background {
background-color: rgba(10, 10, 10, 0.50);
background-color: rgba(10, 10, 10, 0.5);
}
.modal-card-head {
@@ -161,19 +179,25 @@ class WrModal extends LitElement
.modal-card {
width: 100%;
max-width: var(--modal-width, 640px)
max-width: var(--modal-width, 640px);
}
`);
}
render() {
return html`
<div class="modal is-active">
<div class="modal-background" @click="${() => !this.noBgClose && this.onClose()}"></div>
return html` <div class="modal is-active">
<div
class="modal-background"
@click="${() => !this.noBgClose && this.onClose()}"
></div>
<div class="modal-card">
<header class="modal-card-head ${this.bgClass}">
<p class="modal-card-title is-3">${this.title}</p>
<button class="delete" aria-label="close" @click="${this.onClose}"></button>
<button
class="delete"
aria-label="close"
@click="${this.onClose}"
></button>
</header>
<section class="modal-card-body">
<slot></slot>
@@ -192,5 +216,20 @@ customElements.define("wr-anim-logo", AnimLogo);
customElements.define("wr-modal", WrModal);
export { wrapCss, IS_APP, VERSION, clickOnSpacebarPress, rwpLogo, FaIcon, AnimLogo, WrModal,
LitElement, html, css, unsafeCSS, unsafeSVG, apiPrefix, replayPrefix };
export {
wrapCss,
IS_APP,
VERSION,
clickOnSpacebarPress,
rwpLogo,
FaIcon,
AnimLogo,
WrModal,
LitElement,
html,
css,
unsafeCSS,
unsafeSVG,
apiPrefix,
replayPrefix,
};
+81 -32
View File
@@ -8,10 +8,8 @@ import { getReplayLink } from "./pageutils";
import { wrapCss } from "./misc";
// ===========================================================================
class PageEntry extends LitElement
{
class PageEntry extends LitElement {
constructor() {
super();
this.query = "";
@@ -63,7 +61,7 @@ class PageEntry extends LitElement
}
.check-select {
padding: 0 1.0em 0 0.5em;
padding: 0 1em 0 0.5em;
height: 100%;
margin: auto 0 auto 0;
}
@@ -80,7 +78,6 @@ class PageEntry extends LitElement
margin-bottom: calc(-0.75rem + 2px);
}
.favicon {
width: 24px !important;
height: 24px !important;
@@ -180,22 +177,29 @@ class PageEntry extends LitElement
const p = this.page;
const date = this.page.date;
const hasSize = typeof(p.size) === "number";
const hasSize = typeof p.size === "number";
const editable = this.editable && !this.isSidebar;
return html`
${editable ? html`
<div class="check-select">
${editable
? html` <div class="check-select">
<label class="checkbox">
<input @change=${this.onSendSelToggle} type="checkbox" .checked="${this.selected}">
<input
@change=${this.onSendSelToggle}
type="checkbox"
.checked="${this.selected}"
/>
</label>
</div>` : ""}
</div>`
: ""}
<div class="columns">
${this.index ? html`
${this.index
? html`
<div class="column col-index is-1 is-size-7">${this.index}.</div>
` : ""}
`
: ""}
<div class="column col-date is-2">
<div>${date ? date.toLocaleDateString() : ""}</div>
<div>${date ? date.toLocaleTimeString() : ""}</div>
@@ -204,38 +208,71 @@ class PageEntry extends LitElement
<div class="media">
<figure class="media-left">
<p class="">
${this.iconValid ? html`
<img class="favicon" @error="${() => this.iconValid = false}" src="${this.replayPrefix}/${this.page.timestamp}id_/${p.favIconUrl}"/>` : html`
<span class="favicon"></span>`}
${this.iconValid
? html` <img
class="favicon"
@error="${() => (this.iconValid = false)}"
src="${this.replayPrefix}/${this.page
.timestamp}id_/${p.favIconUrl}"
/>`
: html` <span class="favicon"></span>`}
</p>
</figure>
<div class="media-content ${this.isCurrent ? "current" : ""}">
<div role="heading" aria-level="${this.isSidebar ? "4" : "3"}">
<a @dblclick="${this.onReload}" @click="${this.onReplay}" href="${getReplayLink("pages", this.page.url, this.page.timestamp)}">
<a
@dblclick="${this.onReload}"
@click="${this.onReplay}"
href="${getReplayLink(
"pages",
this.page.url,
this.page.timestamp,
)}"
>
<p class="is-size-6 has-text-weight-bold has-text-link text">
<keyword-mark keywords="${this.query}">${p.title || p.url}</keyword-mark>
<keyword-mark keywords="${this.query}"
>${p.title || p.url}</keyword-mark
>
</p>
<p class="has-text-dark text">
<keyword-mark keywords="${this.query}"
>${p.url}</keyword-mark
>
</p>
<p class="has-text-dark text"><keyword-mark keywords="${this.query}">${p.url}</keyword-mark></p>
<p class="has-text-grey-dark text is-inline-date">
${date ? date.toLocaleString() : ""}
</p>
</a>
${this.textSnippet ? html`
<div class="text"><keyword-mark keywords="${this.query}">${this.textSnippet}</keyword-mark></div>` : html``}
${this.textSnippet
? html` <div class="text">
<keyword-mark keywords="${this.query}"
>${this.textSnippet}</keyword-mark
>
</div>`
: html``}
</div>
${hasSize ? html`
<div class="media-right" style="margin-right: 2em">
${hasSize
? html` <div class="media-right" style="margin-right: 2em">
${prettyBytes(p.size)}
</div>` : ""}
</div>`
: ""}
</div>
</div>
</div>
${editable ? html`
${!this.deleting ? html`
<button @click="${this.onSendDeletePage}" class="delete delete-button"></button>` : html`
<button class="button is-loading delete-button is-static"></button>
`}` : ""}
${editable
? html` ${!this.deleting
? html` <button
@click="${this.onSendDeletePage}"
class="delete delete-button"
></button>`
: html`
<button
class="button is-loading delete-button is-static"
></button>
`}`
: ""}
</div>
`;
}
@@ -245,7 +282,9 @@ class PageEntry extends LitElement
return;
}
const resp = await fetch(`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`);
const resp = await fetch(
`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`,
);
if (resp.status != 200) {
this.favIconData = null;
@@ -256,7 +295,9 @@ class PageEntry extends LitElement
const mime = resp.headers.get("content-type");
try {
this.favIconData = `data:${mime};base64,${btoa(String.fromCharCode.apply(null, payload))}`;
this.favIconData = `data:${mime};base64,${btoa(
String.fromCharCode.apply(null, payload),
)}`;
} catch (e) {
console.log(e);
this.favIconData = null;
@@ -324,7 +365,13 @@ class PageEntry extends LitElement
}
sendChangeEvent(data, reload) {
this.dispatchEvent(new CustomEvent("coll-tab-nav", {bubbles: true, composed: true, detail: {data, reload}}));
this.dispatchEvent(
new CustomEvent("coll-tab-nav", {
bubbles: true,
composed: true,
detail: { data, reload },
}),
);
}
onSendDeletePage() {
@@ -335,7 +382,9 @@ class PageEntry extends LitElement
onSendSelToggle(event) {
const page = this.page.id;
const selected = event.currentTarget.checked;
this.dispatchEvent(new CustomEvent("sel-page", {detail: {page, selected}}));
this.dispatchEvent(
new CustomEvent("sel-page", { detail: { page, selected } }),
);
}
}
+271 -108
View File
@@ -14,10 +14,8 @@ import fasAngleDown from "@fortawesome/fontawesome-free/svgs/solid/angle-down.sv
import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.svg";
// ===========================================================================
class Pages extends LitElement
{
class Pages extends LitElement {
constructor() {
super();
this.filteredPages = [];
@@ -58,17 +56,17 @@ class Pages extends LitElement
static get sortKeys() {
return [
{
"key": "",
"name": "Best Match",
key: "",
name: "Best Match",
},
{
"key": "title",
"name": "Title"
key: "title",
name: "Title",
},
{
"key": "date",
"name": "Date"
}
key: "date",
name: "Date",
},
];
}
@@ -104,7 +102,7 @@ class Pages extends LitElement
editing: { type: Boolean },
toDeletePages: { type: Object },
toDeletePage: { type: Object }
toDeletePage: { type: Object },
};
}
@@ -119,10 +117,8 @@ class Pages extends LitElement
async updated(changedProperties) {
if (changedProperties.has("collInfo")) {
this.updateTextSearch();
} else if (changedProperties.has("query")) {
this.filter();
} else if (changedProperties.has("currList")) {
this.filter();
} else if (changedProperties.has("showAllPages")) {
@@ -154,7 +150,11 @@ class Pages extends LitElement
//if (await this.updateComplete) {
const selected = this.renderRoot.querySelector(".current");
if (selected) {
const opts = {behavior: "smooth", block: "nearest", inline: "nearest"};
const opts = {
behavior: "smooth",
block: "nearest",
inline: "nearest",
};
setTimeout(() => selected.scrollIntoView(opts), 100);
}
//}
@@ -180,7 +180,7 @@ class Pages extends LitElement
this.loading = true;
if (this.flex && this.query && this.textPages) {
const results = await this.flex.searchAsync(this.query, 25);
this.filteredPages = results.map(inx => this.textPages[inx]);
this.filteredPages = results.map((inx) => this.textPages[inx]);
} else if (this.showAllPages && this.hasExtraPages) {
this.filteredPages = [...this.textPages];
} else {
@@ -205,7 +205,9 @@ class Pages extends LitElement
}
async filterCurated() {
const resp = await fetch(`${this.collInfo.apiPrefix}/curated/${this.currList}`);
const resp = await fetch(
`${this.collInfo.apiPrefix}/curated/${this.currList}`,
);
const json = await resp.json();
const curated = json.curated;
@@ -223,10 +225,14 @@ class Pages extends LitElement
this.flex = flex;
this.textPages = pages;
this.hasExtraPages = this.textPages && this.collInfo && this.collInfo.pages &&
this.hasExtraPages =
this.textPages &&
this.collInfo &&
this.collInfo.pages &&
this.textPages.length > this.collInfo.pages.length;
return Promise.all(pages.map((page, index) => {
return Promise.all(
pages.map((page, index) => {
let text = page.url;
if (page.title) {
text += " " + page.title;
@@ -235,7 +241,8 @@ class Pages extends LitElement
text += " " + page.text;
}
return flex.addAsync(index, text);
}));
}),
);
}
async updateTextSearch() {
@@ -272,10 +279,8 @@ class Pages extends LitElement
}
await this.addPages(lines);
} catch (e) {
console.warn(e);
} finally {
if (count === 0) {
await this.addPages(this.collInfo.pages);
@@ -298,7 +303,8 @@ class Pages extends LitElement
box-sizing: border-box !important;
}
div[role="main"], #contents div[role="complementary"] {
div[role="main"],
#contents div[role="complementary"] {
height: 100%;
}
@@ -342,7 +348,7 @@ class Pages extends LitElement
.index-bar-title {
font-size: 1.25rem;
text-transform: uppercase;
margin-bottom: 1.0rem;
margin-bottom: 1rem;
word-break: break-word;
}
@@ -365,7 +371,7 @@ class Pages extends LitElement
}
.index-bar-menu {
margin-top: 1.0rem;
margin-top: 1rem;
}
#filter-label {
@@ -408,7 +414,8 @@ class Pages extends LitElement
display: block !important;
}
:host(.sidebar) .columns.is-hidden-mobile, :host(.sidebar) .is-hidden-mobile {
:host(.sidebar) .columns.is-hidden-mobile,
:host(.sidebar) .is-hidden-mobile {
display: none !important;
}
@@ -427,7 +434,7 @@ class Pages extends LitElement
flex-direction: column;
flex: auto;
padding-bottom: 1.0em;
padding-bottom: 1em;
min-height: 0px;
}
@@ -441,7 +448,7 @@ class Pages extends LitElement
.is-disabled {
pointer-events: none;
opacity: .65;
opacity: 0.65;
}
.page-header {
@@ -451,12 +458,12 @@ class Pages extends LitElement
width: 100%;
min-height: fit-content;
margin-bottom: 1.0em;
margin-bottom: 1em;
border-bottom: 3px solid rgb(237, 237, 237);
}
.check-select {
padding: 0 1.0em 0 0.5em;
padding: 0 1em 0 0.5em;
}
.search-bar {
@@ -497,7 +504,7 @@ class Pages extends LitElement
${prefix} .mobile-header {
margin: 0.5rem;
margin-left: 1.0rem;
margin-left: 1rem;
align-items: center;
display: flex;
justify-content: space-between;
@@ -507,8 +514,9 @@ class Pages extends LitElement
}
${prefix} .menu {
font-size: 0.80rem;
}`;
font-size: 0.8rem;
}
`;
}
onSelectList(event) {
@@ -525,64 +533,126 @@ class Pages extends LitElement
const currList = this.currList;
return html`
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "2": "1"}">
<div
class="is-sr-only"
role="heading"
aria-level="${this.isSidebar ? "2" : "1"}"
>
Pages in ${this.collInfo.title}
</div>
<div class="search-bar notification is-marginless">
${this.isSidebar ? html `<h3 class="is-sr-only">Search and Filter Pages</h3>` : ""}
${this.isSidebar
? html`<h3 class="is-sr-only">Search and Filter Pages</h3>`
: ""}
<div class="field flex-auto">
<div class="control has-icons-left ${this.loading ? "is-loading" : ""}">
<input type="search" class="input" @input="${this.onChangeQuery}" .value="${this.query}" type="text"
placeholder="Search by Page URL, Title or Text">
<span class="icon is-left"><fa-icon .svg="${fasSearch}" aria-hidden="true"></fa-icon></span>
<div
class="control has-icons-left ${this.loading ? "is-loading" : ""}"
>
<input
type="search"
class="input"
@input="${this.onChangeQuery}"
.value="${this.query}"
type="text"
placeholder="Search by Page URL, Title or Text"
/>
<span class="icon is-left"
><fa-icon .svg="${fasSearch}" aria-hidden="true"></fa-icon
></span>
</div>
</div>
</div>
<div class="main columns">
<div class="column index-bar is-one-fifth ${this.isSidebar ? "is-hidden-mobile" : ""}">
${this.editable && this.editing ? html`
<form @submit="${this.onUpdateTitle}"><input id="titleEdit" class="input" value="${this.collInfo.title}" @blur="${this.onUpdateTitle}"></form>
` : html`
<div class="index-bar-title" @dblclick="${() => this.editing = true}">${this.collInfo.title}</div>`}
${this.editable ? html`<fa-icon class="editIcon" .svg="${fasEdit}"></fa-icon>` : html``}
${this.hasExtraPages ? html`
<span class="check-select">
<div
class="column index-bar is-one-fifth ${this.isSidebar
? "is-hidden-mobile"
: ""}"
>
${this.editable && this.editing
? html`
<form @submit="${this.onUpdateTitle}">
<input
id="titleEdit"
class="input"
value="${this.collInfo.title}"
@blur="${this.onUpdateTitle}"
/>
</form>
`
: html` <div
class="index-bar-title"
@dblclick="${() => (this.editing = true)}"
>
${this.collInfo.title}
</div>`}
${this.editable
? html`<fa-icon class="editIcon" .svg="${fasEdit}"></fa-icon>`
: html``}
${this.hasExtraPages
? html` <span class="check-select">
<label class="checkbox">
<input @change=${e => this.showAllPages = e.currentTarget.checked} type="checkbox" .checked="${this.showAllPages}">
<input
@change=${(e) =>
(this.showAllPages = e.currentTarget.checked)}
type="checkbox"
.checked="${this.showAllPages}"
/>
Show Non-Seed Pages
</label>
</span>` : ""}
</span>`
: ""}
<span class="num-results is-hidden-mobile" aria-live="polite" aria-atomic="true">${this.formatResults()}</span>
<span
class="num-results is-hidden-mobile"
aria-live="polite"
aria-atomic="true"
>${this.formatResults()}</span
>
${this.editable ? html`
<div class="index-bar-actions">
${this.editable
? html` <div class="index-bar-actions">
${this.renderDownloadMenu()}
</div>` : ""}
${this.collInfo.lists.length ? html`
</div>`
: ""}
${this.collInfo.lists.length
? html`
<p id="filter-label" class="menu-label">Filter By List:</p>
<div class="index-bar-menu menu">
<ul class="menu-list">
<li>
<a href="#list-0" data-list="0" class="${currList === 0 ? "is-active" : ""}"
@click=${this.onSelectList}><i>All Pages</i></a>
<a
href="#list-0"
data-list="0"
class="${currList === 0 ? "is-active" : ""}"
@click=${this.onSelectList}
><i>All Pages</i></a
>
</li>
${this.collInfo.lists.map(list => html`
<li>
<a @click=${this.onSelectList} href="#list-${list.id}"
${this.collInfo.lists.map(
(list) =>
html` <li>
<a
@click=${this.onSelectList}
href="#list-${list.id}"
data-list="${list.id}"
class="${currList === list.id ? "is-active" : ""}">${list.title}</a>
</li>`)}
class="${currList === list.id ? "is-active" : ""}"
>${list.title}</a
>
</li>`,
)}
</ul>
</div>
` : ""}
`
: ""}
</div>
<div class="column main-content">
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "3": "2"}">Page List</div>
<div
class="is-sr-only"
role="heading"
aria-level="${this.isSidebar ? "3" : "2"}"
>
Page List
</div>
${this.renderPages()}
</div>
</div>
@@ -594,7 +664,11 @@ class Pages extends LitElement
return html`
<div class="dropdown ${this.menuActive ? "is-active" : ""}">
<div class="dropdown-trigger">
<button @click="${this.onMenu}" class="button is-small" aria-haspopup="true" aria-expanded="${this.menuActive}" aria-controls="dropdown-menu">
<button @click="${
this.onMenu
}" class="button is-small" aria-haspopup="true" aria-expanded="${
this.menuActive
}" aria-controls="dropdown-menu">
<span>Download</span>
<span class="icon is-small">
<fa-icon .svg="${fasAngleDown} aria-hidden="true"></fa-icon>
@@ -607,19 +681,25 @@ class Pages extends LitElement
@click="${(e) => this.onDownload(e, "wacz")}"
@keyup="${clickOnSpacebarPress}"
class="dropdown-item">
Download ${this.selectedPages.size === 0 ? "All" : "Selected"} as WACZ (Web Archive Collection Zip)
Download ${
this.selectedPages.size === 0 ? "All" : "Selected"
} as WACZ (Web Archive Collection Zip)
</a>
<a role="button" href="#"
@click="${(e) => this.onDownload(e, "warc")}"
@keyup="${clickOnSpacebarPress}"
class="dropdown-item">
Download ${this.selectedPages.size === 0 ? "All" : "Selected"} as WARC 1.1 Only
Download ${
this.selectedPages.size === 0 ? "All" : "Selected"
} as WARC 1.1 Only
</a>
<a role="button" href="#"
@click="${(e) => this.onDownload(e, "warc1.0")}"
@keyup="${clickOnSpacebarPress}"
class="dropdown-item">
Download ${this.selectedPages.size === 0 ? "All" : "Selected"} as WARC 1.0 Only
Download ${
this.selectedPages.size === 0 ? "All" : "Selected"
} as WARC 1.0 Only
</a>
</div>
</div>
@@ -628,31 +708,77 @@ class Pages extends LitElement
renderPageHeader() {
return html`
${!this.isSidebar && this.editable && this.filteredPages.length ? html`
<div class="check-select">
${!this.isSidebar && this.editable && this.filteredPages.length
? html` <div class="check-select">
<label class="checkbox">
<input @change=${this.onSelectAll} type="checkbox" .checked="${this.allSelected}">
<input
@change=${this.onSelectAll}
type="checkbox"
.checked="${this.allSelected}"
/>
</label>
</div>` : html``}
</div>`
: html``}
<div class="header columns is-hidden-mobile">
${this.query ? html`
<a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="" class="column is-1 ${this.sortKey === "" ? (this.sortDesc ? "desc" : "asc") : ""}">Match</a>` : ""}
${this.query
? html` <a
role="button"
href="#"
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key=""
class="column is-1 ${this.sortKey === ""
? this.sortDesc
? "desc"
: "asc"
: ""}"
>Match</a
>`
: ""}
<a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="date" class="column is-2 ${this.sortKey === "date" ? (this.sortDesc ? "desc" : "asc") : ""}">Date</a>
<a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="title" class="column is-6 pagetitle ${this.sortKey === "title" ? (this.sortDesc ? "desc" : "asc") : ""}">Page Title</a>
<a
role="button"
href="#"
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="date"
class="column is-2 ${this.sortKey === "date"
? this.sortDesc
? "desc"
: "asc"
: ""}"
>Date</a
>
<a
role="button"
href="#"
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
data-key="title"
class="column is-6 pagetitle ${this.sortKey === "title"
? this.sortDesc
? "desc"
: "asc"
: ""}"
>Page Title</a
>
</div>
<div class="is-hidden-tablet mobile-header">
<div class="num-results" aria-live="polite" aria-atomic="true">${this.formatResults()}</div>
<wr-sorter id="pages"
<div class="num-results" aria-live="polite" aria-atomic="true">
${this.formatResults()}
</div>
<wr-sorter
id="pages"
.sortKey="${this.sortKey}"
.sortDesc="${this.sortDesc}"
.sortKeys="${Pages.sortKeys}"
.data="${this.filteredPages}"
pageResults="100"
@sort-changed="${this.onSortChanged}"
class="${this.filteredPages.length ? "" : "is-hidden"}">
class="${this.filteredPages.length ? "" : "is-hidden"}"
>
</wr-sorter>
</div>
`;
@@ -663,17 +789,34 @@ class Pages extends LitElement
return html``;
}
return html`
<wr-modal bgClass="has-background-grey-lighter" @modal-closed="${() => this.toDeletePages = this.toDeletePage = null}" title="Confirm Delete">
${this.toDeletePage ? html`
<p>Are you sure you want to delete the page <b>${this.toDeletePage.title}</b>?
(Size: <b>${prettyBytes(this.toDeletePage.size)}</b>)</p>` : html`
<p>Are you sure you want to delete the <b>${this.toDeletePages.size}</b> selected pages?
return html` <wr-modal
bgClass="has-background-grey-lighter"
@modal-closed="${() => (this.toDeletePages = this.toDeletePage = null)}"
title="Confirm Delete"
>
${this.toDeletePage
? html` <p>
Are you sure you want to delete the page
<b>${this.toDeletePage.title}</b>? (Size:
<b>${prettyBytes(this.toDeletePage.size)}</b>)
</p>`
: html`
<p>
Are you sure you want to delete the
<b>${this.toDeletePages.size}</b> selected pages?
</p>
`}
<p>This operation can not be undone.</p>
<button @click="${this.onDeletePages}"class="button is-danger">Delete</button>
<button @click="${() => this.toDeletePages = this.toDeletePage = null}" class="button">Cancel</button>
<button @click="${this.onDeletePages}" class="button is-danger">
Delete
</button>
<button
@click="${() => (this.toDeletePages = this.toDeletePage = null)}"
class="button"
>
Cancel
</button>
</wr-modal>`;
}
@@ -683,7 +826,7 @@ class Pages extends LitElement
let ts = page.timestamp;
if (!ts && page.date) {
ts = getTS(page.date);
} else if (typeof(page.ts) === "string") {
} else if (typeof page.ts === "string") {
ts = getTS(page.ts);
}
return ts === this.ts;
@@ -700,12 +843,11 @@ class Pages extends LitElement
${this.renderPageHeader()}
</div>
<ul class="scroller" @scroll="${this.onScroll}">
${this.sortedPages.length ? html`
${this.sortedPages.map((p, i) => {
${this.sortedPages.length
? html` ${this.sortedPages.map((p, i) => {
const selected = this.selectedPages.has(p.id);
return html`
<li class="page-entry ${selected ? "selected" : ""}">
return html` <li class="page-entry ${selected ? "selected" : ""}">
<wr-page-entry
.index="${this.query || this.isSidebar ? i + 1 : 0}"
.editable="${this.editable}"
@@ -721,7 +863,9 @@ class Pages extends LitElement
class="${this.isSidebar ? "sidebar" : ""}"
>
</wr-page-entry>
</li>`; })}` : html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`}
</li>`;
})}`
: html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`}
</ul>
`;
}
@@ -743,11 +887,15 @@ class Pages extends LitElement
const body = JSON.stringify({ title });
fetch(`${this.collInfo.apiPrefix}/metadata`, {method: "POST", body}).then((res) => {
fetch(`${this.collInfo.apiPrefix}/metadata`, { method: "POST", body }).then(
(res) => {
if (res.status === 200) {
this.dispatchEvent(new CustomEvent("coll-update", {detail: {title}}));
this.dispatchEvent(
new CustomEvent("coll-update", { detail: { title } }),
);
}
});
},
);
}
onMenu(event) {
@@ -755,9 +903,13 @@ class Pages extends LitElement
this.menuActive = !this.menuActive;
if (this.menuActive) {
document.addEventListener("click", () => {
document.addEventListener(
"click",
() => {
this.menuActive = false;
}, {once: true});
},
{ once: true },
);
}
}
@@ -786,7 +938,7 @@ class Pages extends LitElement
} else {
this.selectedPages.delete(page);
}
this.allSelected = (this.selectedPages.size === this.sortedPages.length);
this.allSelected = this.selectedPages.size === this.sortedPages.length;
this.requestUpdate();
}
@@ -796,7 +948,7 @@ class Pages extends LitElement
this.selectedPages.clear();
} else {
//this.selectedPages = new Set();
this.sortedPages.forEach(p => {
this.sortedPages.forEach((p) => {
this.selectedPages.add(p.id);
});
}
@@ -810,7 +962,10 @@ class Pages extends LitElement
const selected = this.selectedPages.size > 0;
const params = new URLSearchParams();
params.set("pages", selected ? Array.from(this.selectedPages.keys()).join(",") : "all");
params.set(
"pages",
selected ? Array.from(this.selectedPages.keys()).join(",") : "all",
);
params.set("format", format);
if (this.collInfo.filename) {
params.set("filename", this.collInfo.filename);
@@ -844,7 +999,9 @@ class Pages extends LitElement
}
for (const id of this.toDeletePages) {
const resp = await fetch(`${this.collInfo.apiPrefix}/page/${id}`, {method: "DELETE"});
const resp = await fetch(`${this.collInfo.apiPrefix}/page/${id}`, {
method: "DELETE",
});
const json = await resp.json();
if (json.error) {
@@ -894,7 +1051,10 @@ class Pages extends LitElement
getNoResultsMessage() {
if (!this.collInfo || !this.collInfo.pages.length) {
return html`<span class="fix-text-wrapping">No Pages are defined in this archive. The archive may be empty. <a href="#view=resources">Try browsing by URL</a>.</span>`;
return html`<span class="fix-text-wrapping"
>No Pages are defined in this archive. The archive may be empty.
<a href="#view=resources">Try browsing by URL</a>.</span
>`;
}
if (this.updatingSearch) {
@@ -906,7 +1066,10 @@ class Pages extends LitElement
}
if (this.query) {
return html `<span class="fix-text-wrapping">No matching pages found. Try changing the search query, or <a href="#view=resources">browse by URL</a>.</span>`;
return html`<span class="fix-text-wrapping"
>No matching pages found. Try changing the search query, or
<a href="#view=resources">browse by URL</a>.</span
>`;
}
return "No Pages Found";
@@ -914,7 +1077,8 @@ class Pages extends LitElement
onScroll(event) {
const element = event.currentTarget;
const diff = (element.scrollHeight - element.scrollTop) - element.clientHeight;
const diff =
element.scrollHeight - element.scrollTop - element.clientHeight;
if (diff < 40) {
const sorter = this.renderRoot.querySelector("wr-sorter");
if (sorter) {
@@ -924,7 +1088,6 @@ class Pages extends LitElement
}
}
customElements.define("wr-page-view", Pages);
export { Pages };
+26 -15
View File
@@ -3,11 +3,12 @@ async function digestMessage(message, hashtype) {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest(hashtype, msgUint8); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); // convert bytes to hex string
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join(""); // convert bytes to hex string
return hashHex;
}
// ===========================================================================
function tsToDate(ts) {
if (!ts) {
@@ -18,12 +19,19 @@ function tsToDate(ts) {
ts += "00000101000000".substr(ts.length);
}
const datestr = (ts.substring(0, 4) + "-" +
ts.substring(4, 6) + "-" +
ts.substring(6, 8) + "T" +
ts.substring(8, 10) + ":" +
ts.substring(10, 12) + ":" +
ts.substring(12, 14) + "-00:00");
const datestr =
ts.substring(0, 4) +
"-" +
ts.substring(4, 6) +
"-" +
ts.substring(6, 8) +
"T" +
ts.substring(8, 10) +
":" +
ts.substring(10, 12) +
":" +
ts.substring(12, 14) +
"-00:00";
return new Date(datestr);
}
@@ -37,17 +45,15 @@ function getPageDateTS(page) {
// leave date unchanged in case of error
}
const timestamp = (date && !isNaN(date)) ? getTS(date.toISOString()) : "";
const timestamp = date && !isNaN(date) ? getTS(date.toISOString()) : "";
return { date, timestamp };
}
// ===========================================================================
function getTS(iso) {
return iso.replace(/[-:T]/g, "").slice(0, 14);
}
// ===========================================================================
function getReplayLink(view, url, ts) {
const params = new URLSearchParams();
@@ -57,7 +63,6 @@ function getReplayLink(view, url, ts) {
return "#" + params.toString();
}
// ===========================================================================
async function sourceToId(url) {
try {
@@ -72,7 +77,6 @@ async function sourceToId(url) {
return { url, coll };
}
// ===========================================================================
// simple parse of scheme, host, rest of path
// not using URL due to different browser behavior
@@ -105,5 +109,12 @@ function parseURLSchemeHostPath(url) {
return { scheme, host, path };
}
export { digestMessage, tsToDate, getTS, getPageDateTS, getReplayLink, sourceToId, parseURLSchemeHostPath };
export {
digestMessage,
tsToDate,
getTS,
getPageDateTS,
getReplayLink,
sourceToId,
parseURLSchemeHostPath,
};
+133 -53
View File
@@ -1,11 +1,8 @@
import { LitElement, html, css } from "lit";
import { wrapCss, rwpLogo } from "./misc";
// ===========================================================================
class Replay extends LitElement
{
class Replay extends LitElement {
constructor() {
super();
this.replayUrl = "";
@@ -39,21 +36,30 @@ class Replay extends LitElement
iframeUrl: { type: String },
showAuth: { type: Boolean },
authFileHandle: { type: Object }
authFileHandle: { type: Object },
};
}
firstUpdated() {
window.addEventListener("message", (event) => this.onReplayMessage(event));
navigator.serviceWorker.addEventListener("message", (event) => this.handleAuthMessage(event));
navigator.serviceWorker.addEventListener("message", (event) =>
this.handleAuthMessage(event),
);
}
async handleAuthMessage(event) {
if (event.data.type === "authneeded" && this.collInfo && event.data.coll === this.collInfo.coll) {
if (
event.data.type === "authneeded" &&
this.collInfo &&
event.data.coll === this.collInfo.coll
) {
if (event.data.fileHandle) {
this.authFileHandle = event.data.fileHandle;
try {
if (await this.authFileHandle.requestPermission({mode: "read"}) === "granted") {
if (
(await this.authFileHandle.requestPermission({ mode: "read" })) ===
"granted"
) {
this.showAuth = false;
this.reauthWait = null;
this.refresh();
@@ -75,18 +81,24 @@ class Replay extends LitElement
}
doSetIframeUrl() {
this.iframeUrl = this.url ? `${this.collInfo.replayPrefix}/${this.ts || ""}mp_/${this.url}` : "";
this.iframeUrl = this.url
? `${this.collInfo.replayPrefix}/${this.ts || ""}mp_/${this.url}`
: "";
}
updated(changedProperties) {
if (changedProperties.has("sourceUrl") || changedProperties.has("collInfo")) {
if (
changedProperties.has("sourceUrl") ||
changedProperties.has("collInfo")
) {
this.reauthWait = null;
}
if (this.url &&
((this.replayUrl != this.url) || (this.replayTS != this.ts)) &&
(changedProperties.has("url") || changedProperties.has("ts"))) {
if (
this.url &&
(this.replayUrl != this.url || this.replayTS != this.ts) &&
(changedProperties.has("url") || changedProperties.has("ts"))
) {
this.replayUrl = this.url;
this.replayTS = this.ts;
this.showAuth = false;
@@ -98,29 +110,48 @@ class Replay extends LitElement
this.waitForLoad();
const detail = { title: "Archived Page", replayTitle: false };
this.dispatchEvent(new CustomEvent("update-title", {bubbles: true, composed: true, detail}));
this.dispatchEvent(
new CustomEvent("update-title", {
bubbles: true,
composed: true,
detail,
}),
);
}
if ((this.replayUrl && changedProperties.has("replayUrl")) ||
(this.replayTS && changedProperties.has("replayTS"))) {
if (
(this.replayUrl && changedProperties.has("replayUrl")) ||
(this.replayTS && changedProperties.has("replayTS"))
) {
const data = {
url: this.replayUrl,
ts: this.replayTS,
};
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {replaceLoc: true, data}}));
this.dispatchEvent(
new CustomEvent("coll-tab-nav", { detail: { replaceLoc: true, data } }),
);
}
if (this.title && (changedProperties.has("title") || changedProperties.has("actualTS"))) {
if (
this.title &&
(changedProperties.has("title") || changedProperties.has("actualTS"))
) {
const detail = {
title: this.title,
url: this.replayUrl,
// send actual ts even if live
ts: this.actualTS,
replayTitle: true
replayTitle: true,
};
this.dispatchEvent(new CustomEvent("update-title", {bubbles: true, composed: true, detail}));
this.dispatchEvent(
new CustomEvent("update-title", {
bubbles: true,
composed: true,
detail,
}),
);
}
}
@@ -136,7 +167,10 @@ class Replay extends LitElement
const iframe = this.renderRoot.querySelector("iframe");
if (iframe && event.source === iframe.contentWindow) {
if (event.data.wb_type === "load" || event.data.wb_type === "replace-url") {
if (
event.data.wb_type === "load" ||
event.data.wb_type === "replace-url"
) {
this.replayTS = event.data.is_live ? "" : event.data.ts;
this.actualTS = event.data.ts;
this.replayUrl = event.data.url;
@@ -145,7 +179,13 @@ class Replay extends LitElement
if (event.data.icons) {
const icons = event.data.icons;
this.dispatchEvent(new CustomEvent("replay-favicons", {bubbles: true, composed: true, detail: {icons}}));
this.dispatchEvent(
new CustomEvent("replay-favicons", {
bubbles: true,
composed: true,
detail: { icons },
}),
);
}
} else if (event.data.wb_type === "title") {
this.title = event.data.title;
@@ -155,17 +195,19 @@ class Replay extends LitElement
onReAuthed(event) {
this.reauthWait = (async () => {
if (!this.authFileHandle) {
// google drive reauth
const headers = event.detail.headers;
await fetch(`${this.collInfo.apiPrefix}/updateAuth`, {
method: "POST",
body: JSON.stringify({headers})
body: JSON.stringify({ headers }),
});
} else {
if (await this.authFileHandle.requestPermission({mode: "read"}) !== "granted") {
if (
(await this.authFileHandle.requestPermission({ mode: "read" })) !==
"granted"
) {
this.reauthWait = null;
return;
}
@@ -184,15 +226,22 @@ class Replay extends LitElement
this.setLoading();
this._loadPoll = window.setInterval(() => {
const iframe = this.renderRoot.querySelector("iframe");
if (!iframe || !iframe.contentDocument || !iframe.contentWindow ||
(iframe.contentDocument.readyState === "complete" && !iframe.contentWindow._WBWombat)) {
if (
!iframe ||
!iframe.contentDocument ||
!iframe.contentWindow ||
(iframe.contentDocument.readyState === "complete" &&
!iframe.contentWindow._WBWombat)
) {
this.clearLoading(iframe && iframe.contentWindow);
}
}, 5000);
}
clearLoading(iframeWin) {
this.dispatchEvent(new CustomEvent("replay-loading", {detail: {loading: false}}));
this.dispatchEvent(
new CustomEvent("replay-loading", { detail: { loading: false } }),
);
if (this._loadPoll) {
window.clearInterval(this._loadPoll);
@@ -207,7 +256,9 @@ class Replay extends LitElement
}
setLoading() {
this.dispatchEvent(new CustomEvent("replay-loading", {detail: {loading: true}}));
this.dispatchEvent(
new CustomEvent("replay-loading", { detail: { loading: true } }),
);
}
refresh() {
@@ -252,13 +303,13 @@ class Replay extends LitElement
}
.intro-panel .panel-heading {
font-size: 1.0em;
font-size: 1em;
display: inline-block;
}
.iframe-main.modal-bg {
z-index: 200;
background-color: rgba(10, 10, 10, 0.70)
background-color: rgba(10, 10, 10, 0.7);
}
#wrlogo {
@@ -266,7 +317,7 @@ class Replay extends LitElement
}
.intro-panel .panel-block {
padding: 1.0em;
padding: 1em;
flex-direction: column;
line-height: 2.5em;
}
@@ -284,45 +335,74 @@ class Replay extends LitElement
render() {
const title = `Replay of ${this.title ? `${this.title}:` : ""} ${this.url}`;
return html`
return html` <h1 id="replay-heading" class="is-sr-only">${title}</h1>
<h1 id="replay-heading" class="is-sr-only">${title}</h1>
${!this.iframeUrl ? html`
<div class="panel intro-panel">
${!this.iframeUrl
? html` <div class="panel intro-panel">
<p class="panel-heading">Replay Web Page</p>
<div class="panel-block">
<p>Enter a URL above to replay it from the web archive!</p>
<p>(Or, check out <a href="#view=pages">Pages</a> or <a href="#view=resources">URLs</a> to explore the contents of this archive.)</p>
<p>
(Or, check out <a href="#view=pages">Pages</a> or
<a href="#view=resources">URLs</a> to explore the contents of
this archive.)
</p>
</div>
</div>` : html`
</div>`
: html`
<div class="iframe-container">
<iframe class="iframe-main" name="___wb_replay_top_frame" @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
src="${this.iframeUrl}" title="${title}"></iframe>
<iframe
class="iframe-main"
name="___wb_replay_top_frame"
@message="${this.onReplayMessage}"
allow="autoplay 'self'; fullscreen"
allowfullscreen
src="${this.iframeUrl}"
title="${title}"
></iframe>
${this.showAuth ? html`
${this.showAuth
? html`
<div class="iframe-main modal-bg">
<div class="panel intro-panel">
<p class="panel-heading">
<fa-icon id="wrlogo" size="1.5rem" .svg=${rwpLogo} aria-hidden="true"></fa-icon>
<fa-icon
id="wrlogo"
size="1.5rem"
.svg=${rwpLogo}
aria-hidden="true"
></fa-icon>
Authorization Needed
</p>
<div class="panel-block">
${this.authFileHandle ? html`
<p>This archive is loaded from a local file: <b>${this.authFileHandle.name}</b></p>
<p>The browser needs to confirm your permission to continue loading from this file.</p>
<button class="button is-warning is-rounded" @click="${this.onReAuthed}">Show Confirmation</button>
` : html`
<wr-gdrive
${this.authFileHandle
? html`
<p>
This archive is loaded from a local file:
<b>${this.authFileHandle.name}</b>
</p>
<p>
The browser needs to confirm your permission
to continue loading from this file.
</p>
<button
class="button is-warning is-rounded"
@click="${this.onReAuthed}"
>
Show Confirmation
</button>
`
: html` <wr-gdrive
.sourceUrl="${this.sourceUrl}"
.state="trymanual"
.reauth="${true}"
@load-ready="${this.onReAuthed}"/>`}
@load-ready="${this.onReAuthed}"
/>`}
</div>
</div>
</div>
`: ""}
`
: ""}
</div>
`}`;
}
+23 -12
View File
@@ -4,10 +4,8 @@ import { wrapCss } from "./misc";
import fasSortDown from "@fortawesome/fontawesome-free/svgs/solid/sort-down.svg";
import fasSortUp from "@fortawesome/fontawesome-free/svgs/solid/sort-up.svg";
// ===========================================================================
class Sorter extends LitElement
{
class Sorter extends LitElement {
constructor() {
super();
this.sortedData = [];
@@ -79,7 +77,9 @@ class Sorter extends LitElement
return 0;
}
return (this.sortDesc == (first[this.sortKey] < second[this.sortKey])) ? 1 : -1;
return this.sortDesc == first[this.sortKey] < second[this.sortKey]
? 1
: -1;
});
}
@@ -90,7 +90,9 @@ class Sorter extends LitElement
const detail = {
sortKey: this.sortKey,
sortDesc: this.sortDesc,
sortedData: this.numResults ? this.sortedData.slice(0, this.numResults) : this.sortedData
sortedData: this.numResults
? this.sortedData.slice(0, this.numResults)
: this.sortedData,
};
this.dispatchEvent(new CustomEvent("sort-changed", { detail }));
}
@@ -119,17 +121,26 @@ class Sorter extends LitElement
render() {
return html`
<div class="select is-small">
<select id="sort-select" @change=${(e) => this.sortKey = e.currentTarget.value}>
${this.sortKeys.map((sort) => html`
<option value="${sort.key}" ?selected="${sort.key === this.sortKey}">Sort By: ${sort.name}
<select id="sort-select" @change=${(e) =>
(this.sortKey = e.currentTarget.value)}>
${this.sortKeys.map(
(sort) => html`
<option value="${sort.key}" ?selected="${sort.key === this.sortKey}">
Sort By: ${sort.name}
</option>
`)}
`,
)}
</select>
</div>
<button @click=${() => this.sortDesc = !this.sortDesc} class="button is-small">
<button @click=${() =>
(this.sortDesc = !this.sortDesc)} class="button is-small">
<span>Order:</span>
<span class="is-sr-only">${this.sortDesc ? "Ascending" : "Descending"}</span>
<span class="icon"><fa-icon aria-hidden="true" .svg=${this.sortDesc ? fasSortUp : fasSortDown}></span>
<span class="is-sr-only">${
this.sortDesc ? "Ascending" : "Descending"
}</span>
<span class="icon"><fa-icon aria-hidden="true" .svg=${
this.sortDesc ? fasSortUp : fasSortDown
}></span>
</button>`;
}
}
+66 -33
View File
@@ -8,11 +8,8 @@ import { getTS, getReplayLink } from "./pageutils";
import Split from "split.js";
// ===========================================================================
class Story extends LitElement
{
class Story extends LitElement {
constructor() {
super();
@@ -42,12 +39,13 @@ class Story extends LitElement
currList: { type: Number },
isSidebar: { type: Boolean },
splitDirection: { type: Boolean }
splitDirection: { type: Boolean },
};
}
recalcSplitter(width) {
this.splitDirection = this.isSidebar || width < 769 ? "vertical" : "horizontal";
this.splitDirection =
this.isSidebar || width < 769 ? "vertical" : "horizontal";
}
firstUpdated() {
@@ -65,7 +63,10 @@ class Story extends LitElement
this.doLoadCurated();
}
if (changedProperties.has("collInfo") || changedProperties.has("isSidebar")) {
if (
changedProperties.has("collInfo") ||
changedProperties.has("isSidebar")
) {
this.recalcSplitter(document.documentElement.clientWidth);
}
@@ -99,7 +100,7 @@ class Story extends LitElement
gutterSize: 4,
direction: this.splitDirection
direction: this.splitDirection,
};
this.splitter = Split([sidebar, content], opts);
@@ -165,14 +166,14 @@ class Story extends LitElement
${Story.sidebarStyles(unsafeCSS(":host(.sidebar)"))}
.desc p {
margin-bottom: 1.0em;
margin-bottom: 1em;
}
.columns {
width: 100%;
height: 100%;
justify-self: stretch;
margin: 1.0em 0 0 0 !important;
margin: 1em 0 0 0 !important;
min-height: 0;
}
@@ -218,7 +219,6 @@ class Story extends LitElement
.gutter.gutter-horizontal:hover {
cursor: col-resize;
}
`);
}
@@ -243,15 +243,20 @@ class Story extends LitElement
}
${prefix} .menu {
font-size: 0.80rem;
}`;
font-size: 0.8rem;
}
`;
}
render() {
const currListNum = this.currList;
return html`
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "2": "1"}">
<div
class="is-sr-only"
role="heading"
aria-level="${this.isSidebar ? "2" : "1"}"
>
Story for ${this.collInfo.title}
</div>
<div class="columns">
@@ -259,14 +264,28 @@ class Story extends LitElement
<aside class="menu">
<ul class="menu-list">
<li>
<a href="#list-0" data-list="0" class="${currListNum === 0 ? "is-active" : ""} menu-label is-size-4"
@click=${this.onClickScroll}>${this.collInfo.title}</a>
<ul class="menu-list">${this.collInfo.lists.map(list => html`
<li>
<a @click=${this.onClickScroll} href="#list-${list.id}"
<a
href="#list-0"
data-list="0"
class="${currListNum === 0
? "is-active"
: ""} menu-label is-size-4"
@click=${this.onClickScroll}
>${this.collInfo.title}</a
>
<ul class="menu-list">
${this.collInfo.lists.map(
(list) =>
html` <li>
<a
@click=${this.onClickScroll}
href="#list-${list.id}"
data-list="${list.id}"
class="${currListNum === list.id ? "is-active" : ""}">${list.title}</a>
</li>`)}
class="${currListNum === list.id ? "is-active" : ""}"
>${list.title}</a
>
</li>`,
)}
</ul>
</li>
</ul>
@@ -284,19 +303,22 @@ class Story extends LitElement
}
renderLists() {
return html`
${this.collInfo.lists.map((list) => html`
return html` ${this.collInfo.lists.map(
(list) => html`
<article id="list-${list.id}">
<div class="content">
<hr />
<h3>${list.title}</h3>
<p>${list.desc}</p>
<ol>
${this.curatedPageMap[list.id] ? this.curatedPageMap[list.id].map((p) => this.renderCPage(p)) : html``}
${this.curatedPageMap[list.id]
? this.curatedPageMap[list.id].map((p) => this.renderCPage(p))
: html``}
</ol>
</div>
</article>
`)}`;
`,
)}`;
}
renderCPage(p) {
@@ -304,11 +326,14 @@ class Story extends LitElement
const ts = getTS(date.toISOString());
return html`
<li>
return html` <li>
<div class="content">
<a @click="${this.onReplay}" data-url="${p.url}" data-ts="${ts}"
href="${getReplayLink("story", p.url, ts)}">
<a
@click="${this.onReplay}"
data-url="${p.url}"
data-ts="${ts}"
href="${getReplayLink("story", p.url, ts)}"
>
<p class="is-size-6 has-text-weight-bold has-text-link">${p.title}</p>
<p class="has-text-dark">${p.url}</p>
</a>
@@ -368,11 +393,17 @@ class Story extends LitElement
const currST = scrollable.scrollTop;
if (currST > this.lastST) {
while (next.nextElementSibling && next.nextElementSibling.getBoundingClientRect().top < target) {
while (
next.nextElementSibling &&
next.nextElementSibling.getBoundingClientRect().top < target
) {
next = next.nextElementSibling;
}
} else {
while (next.previousElementSibling && next.previousElementSibling.getBoundingClientRect().bottom >= target) {
while (
next.previousElementSibling &&
next.previousElementSibling.getBoundingClientRect().bottom >= target
) {
next = next.previousElementSibling;
}
}
@@ -383,11 +414,13 @@ class Story extends LitElement
}
}
if ((new Date().getTime() - this.clickTime) < 1000) {
if (new Date().getTime() - this.clickTime < 1000) {
return;
}
const sel = this.renderRoot.querySelector(`a[data-list="${this.currList}"]`);
const sel = this.renderRoot.querySelector(
`a[data-list="${this.currList}"]`,
);
if (sel) {
const opts = { behavior: "smooth", block: "nearest", inline: "nearest" };
sel.scrollIntoView(opts);
+4 -5
View File
@@ -4,14 +4,16 @@ import { SWReplay } from "@webrecorder/wabac/src/swmain";
import { WorkerLoader } from "@webrecorder/wabac/src/loaders";
if (self.registration) {
const staticData = new Map();
const prefix = self.registration.scope;
staticData.set(prefix, { type: "text/html", content: INDEX_HTML });
staticData.set(prefix + "index.html", {type: "text/html", content: INDEX_HTML});
staticData.set(prefix + "index.html", {
type: "text/html",
content: INDEX_HTML,
});
const sp = new URLSearchParams(self.location.search);
@@ -25,6 +27,3 @@ if (self.registration) {
} else {
new WorkerLoader(self);
}
+20 -10
View File
@@ -1,11 +1,14 @@
import { html } from "lit";
import { register } from "register-service-worker";
// ===========================================================================
export class SWManager
{
constructor({name = "sw.js", scope = "./", appName = "ReplayWeb.page", requireSubdomainIframe = false} = {}) {
export class SWManager {
constructor({
name = "sw.js",
scope = "./",
appName = "ReplayWeb.page",
requireSubdomainIframe = false,
} = {}) {
this.name = name;
this.scope = scope;
this.appName = appName;
@@ -37,7 +40,9 @@ export class SWManager
console.error("Error during service worker registration:", error);
this.errorMsg = this.getCrossOriginIframeMsg();
if (!this.errorMsg) {
this.errorMsg = `${this.appName} could not be loaded due to the following error:\n${error.toString()}`;
this.errorMsg = `${
this.appName
} could not be loaded due to the following error:\n${error.toString()}`;
}
reject(this.errorMsg);
@@ -50,7 +55,9 @@ export class SWManager
resolve();
},
error(errMsg) { handleError(errMsg); }
error(errMsg) {
handleError(errMsg);
},
});
return p;
@@ -81,7 +88,6 @@ export class SWManager
getSWErrorMsg() {
if (navigator.serviceWorker) {
// must be loaded from a cross-origin (eg. subdomain)
if (this.requireSubdomainIframe && !this.isCrossOriginIframe()) {
return `Sorry, due to security settings, this ${this.appName} embed only be viewed within a subdomain iframe.`;
@@ -109,7 +115,6 @@ export class SWManager
return `Sorry, ${this.appName} won't work in this browser as Service Workers are not supported in this window.
Please try a different browser.`;
}
renderErrorReport(logo, override) {
@@ -122,11 +127,16 @@ export class SWManager
return html`
<section class="is-fullwidth">
<div class="has-text-centered">
<fa-icon style="margin: 1em;flex-grow: 1;" id="wrlogo" size="2.5rem" .svg=${logo} aria-hidden="true"></fa-icon>
<fa-icon
style="margin: 1em;flex-grow: 1;"
id="wrlogo"
size="2.5rem"
.svg=${logo}
aria-hidden="true"
></fa-icon>
</div>
<div style="white-space: pre-wrap; text-align: center">${msg}</div>
</section>
`;
}
}
+16 -4
View File
@@ -12,7 +12,19 @@ import { Sorter } from "./sorter";
import { URLResources } from "./url-resources";
import { Embed } from "./embed";
export { ReplayWebApp, Chooser, CollIndex, CollInfo, Coll, Story, GDrive,
Loader, Pages, PageEntry,
Replay, Sorter, URLResources, Embed };
export {
ReplayWebApp,
Chooser,
CollIndex,
CollInfo,
Coll,
Story,
GDrive,
Loader,
Pages,
PageEntry,
Replay,
Sorter,
URLResources,
Embed,
};
+140 -55
View File
@@ -7,44 +7,49 @@ import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import "keyword-mark-element/lib/keyword-mark.js";
// ===========================================================================
class URLResources extends LitElement
{
class URLResources extends LitElement {
static get filters() {
return [
{ name: "HTML", filter: "text/html,text/xhtml" },
{ name: "Images", filter: "image/" },
{ name: "Audio/Video", filter: "audio/,video/" },
{ name: "PDF", filter: "application/pdf" },
{name: "Javascript", filter: "application/javascript,application/x-javascript"},
{
name: "Javascript",
filter: "application/javascript,application/x-javascript",
},
{ name: "CSS", filter: "text/css" },
{ name: "Fonts", filter: "font/,application/font-woff" },
{ name: "Plain Text", filter: "text/plain" },
{ name: "JSON", filter: "application/json" },
{name: "DASH/HLS", filter: "application/dash+xml,application/x-mpegURL,application/vnd.apple.mpegurl"},
{name: "All URLs", filter: ""}
{
name: "DASH/HLS",
filter:
"application/dash+xml,application/x-mpegURL,application/vnd.apple.mpegurl",
},
{ name: "All URLs", filter: "" },
];
}
static get sortKeys() {
return [
{
"key": "url",
"name": "URL"
key: "url",
name: "URL",
},
{
"key": "ts",
"name": "Date"
key: "ts",
name: "Date",
},
{
"key": "mime",
"name": "Mime Type",
key: "mime",
name: "Mime Type",
},
{
"key": "status",
"name": "Status"
}
key: "status",
name: "Status",
},
];
}
@@ -82,7 +87,7 @@ class URLResources extends LitElement
sortedResults: { type: Array },
loading: { type: Boolean },
sortKey: { type: String },
sortDesc: { type: Boolean }
sortDesc: { type: Boolean },
};
}
@@ -101,18 +106,23 @@ class URLResources extends LitElement
}
updated(changedProperties) {
if (changedProperties.has("query") ||
if (
changedProperties.has("query") ||
changedProperties.has("urlSearchType") ||
changedProperties.has("currMime")) {
changedProperties.has("currMime")
) {
this.doLoadResources();
const data = {
query: this.query,
urlSearchType: this.urlSearchType,
currMime: this.currMime
currMime: this.currMime,
};
const replaceLoc = !changedProperties.has("currMime") && !changedProperties.has("urlSearchType");
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {replaceLoc, data}}));
const replaceLoc =
!changedProperties.has("currMime") &&
!changedProperties.has("urlSearchType");
this.dispatchEvent(
new CustomEvent("coll-tab-nav", { detail: { replaceLoc, data } }),
);
}
if (changedProperties.has("sortKey") || changedProperties.has("sortDesc")) {
@@ -132,7 +142,7 @@ class URLResources extends LitElement
}
this.loading = true;
let url = (this.urlSearchType !== "contains" ? this.query : "");
let url = this.urlSearchType !== "contains" ? this.query : "";
const prefix = url && this.urlSearchType === "prefix" ? 1 : 0;
// optimization: if not starting with http, likely won't have a match here, so just add https://
@@ -146,7 +156,7 @@ class URLResources extends LitElement
mime,
url,
prefix,
count
count,
});
if (isMore) {
@@ -157,11 +167,13 @@ class URLResources extends LitElement
params.set("fromTs", new Date(last.date).getTime());
}
let resp = await fetch(`${this.collInfo.apiPrefix}/urls?${params.toString()}`);
let resp = await fetch(
`${this.collInfo.apiPrefix}/urls?${params.toString()}`,
);
resp = await resp.json();
this.results = isMore ? this.results.concat(resp.urls) : resp.urls;
// can be more than count if multiple mimes
this.tryMore = (resp.urls.length >= count);
this.tryMore = resp.urls.length >= count;
this.filter();
this.loading = false;
@@ -185,7 +197,7 @@ class URLResources extends LitElement
filter() {
const filteredResults = [];
const filterText = (this.urlSearchType === "contains" ? this.query : "");
const filterText = this.urlSearchType === "contains" ? this.query : "";
for (const result of this.results) {
if (!filterText || result.url.indexOf(filterText) >= 0) {
filteredResults.push(result);
@@ -197,7 +209,8 @@ class URLResources extends LitElement
onScroll(event) {
const element = event.currentTarget;
const diff = (element.scrollHeight - element.scrollTop) - element.clientHeight;
const diff =
element.scrollHeight - element.scrollTop - element.clientHeight;
if (this.tryMore && diff < 40) {
this.doLoadResources(true);
}
@@ -220,7 +233,9 @@ class URLResources extends LitElement
display: none !important;
}
:host(.sidebar) .level, :host(.sidebar) .level-left, :host(.sidebar) .level-right {
:host(.sidebar) .level,
:host(.sidebar) .level-left,
:host(.sidebar) .level-right {
display: block !important;
}
@@ -300,26 +315,40 @@ class URLResources extends LitElement
render() {
return html`
<div role="heading" aria-level="${this.isSidebar ? "2": "1"}" class="is-sr-only">URLs in ${this.collInfo.title}</div>
<div role="heading" aria-level="${
this.isSidebar ? "2" : "1"
}" class="is-sr-only">URLs in ${this.collInfo.title}</div>
<div role="heading" aria-level="${this.isSidebar ? "3": "2"}" class="is-sr-only">Search and Filter</div>
<div role="heading" aria-level="${
this.isSidebar ? "3" : "2"
}" class="is-sr-only">Search and Filter</div>
<div class="notification level is-marginless">
<div class="level-left flex-auto">
<div class="level-item flex-auto">
<span class="is-hidden-mobile">Search:&nbsp;&nbsp;</span>
<div class="select">
<select @change="${this.onChangeTypeSearch}">
${URLResources.filters.map((filter) => html`
<option value="${filter.filter}"
?selected="${filter.filter === this.currMime}">
${URLResources.filters.map(
(filter) => html`
<option
value="${filter.filter}"
?selected="${filter.filter === this.currMime}"
>
${filter.name}
</option>
`)}
`,
)}
</select>
</div>
<div class="field flex-auto">
<div class="control has-icons-left ${this.loading ? "is-loading" : ""}">
<input type="text" class="input" @input="${this.onChangeQuery}" .value="${this.query}" type="text" placeholder="Enter URL to Search">
<div class="control has-icons-left ${
this.loading ? "is-loading" : ""
}">
<input type="text" class="input" @input="${
this.onChangeQuery
}" .value="${
this.query
}" type="text" placeholder="Enter URL to Search">
<span class="icon is-left"><fa-icon .svg="${fasSearch}"/></span>
</div>
</div>
@@ -327,10 +356,18 @@ class URLResources extends LitElement
</div>
<div class="control level-right">
<div style="margin-left: 1em" class="control">
<label class="radio has-text-left"><input type="radio" name="urltype" value="contains" ?checked="${this.urlSearchType === "contains"}" @click="${this.onClickUrlType}">&nbsp;Contains</label>
<label class="radio has-text-left"><input type="radio" name="urltype" value="prefix" ?checked="${this.urlSearchType === "prefix"}" @click="${this.onClickUrlType}">&nbsp;Prefix</label>
<label class="radio has-text-left"><input type="radio" name="urltype" value="exact" ?checked="${this.urlSearchType === "exact"}" @click="${this.onClickUrlType}">&nbsp;Exact</label>
<span id="num-results" class="num-results" is-pulled-right" aria-live="polite" aria-atomic="true">${this.filteredResults.length} Result(s)</span>
<label class="radio has-text-left"><input type="radio" name="urltype" value="contains" ?checked="${
this.urlSearchType === "contains"
}" @click="${this.onClickUrlType}">&nbsp;Contains</label>
<label class="radio has-text-left"><input type="radio" name="urltype" value="prefix" ?checked="${
this.urlSearchType === "prefix"
}" @click="${this.onClickUrlType}">&nbsp;Prefix</label>
<label class="radio has-text-left"><input type="radio" name="urltype" value="exact" ?checked="${
this.urlSearchType === "exact"
}" @click="${this.onClickUrlType}">&nbsp;Exact</label>
<span id="num-results" class="num-results" is-pulled-right" aria-live="polite" aria-atomic="true">${
this.filteredResults.length
} Result(s)</span>
</div>
</div>
</div>
@@ -345,30 +382,78 @@ class URLResources extends LitElement
</wr-sorter>
</div>
<div role="heading" aria-level="${this.isSidebar ? "3": "2"}" id="results-heading" class="is-sr-only">Results</div>
<div role="heading" aria-level="${
this.isSidebar ? "3" : "2"
}" id="results-heading" class="is-sr-only">Results</div>
<table class="all-results" aria-labelledby="results-heading num-results">
<thead>
<tr class="columns results-head has-text-weight-bold">
<th scope="col" class="column col-url is-6 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="url" class="${this.sortKey === "url" ? (this.sortDesc ? "desc" : "asc") : ""}">URL</a></th>
<th scope="col" class="column col-ts is-2 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="ts" class="${this.sortKey === "ts" ? (this.sortDesc ? "desc" : "asc") : ""}">Date</a></th>
<th scope="col" class="column col-mime is-3 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="mime" class="${this.sortKey === "mime" ? (this.sortDesc ? "desc" : "asc") : ""}">Mime Type</a></th>
<th scope="col" class="column col-status is-1 is-hidden-mobile"><a role="button" href="#" @click="${this.onSort}" @keyup="${clickOnSpacebarPress}" data-key="status" class="${this.sortKey === "status" ? (this.sortDesc ? "desc" : "asc") : ""}">Status</a></th>
<th scope="col" class="column col-url is-6 is-hidden-mobile"><a role="button" href="#" @click="${
this.onSort
}" @keyup="${clickOnSpacebarPress}" data-key="url" class="${
this.sortKey === "url" ? (this.sortDesc ? "desc" : "asc") : ""
}">URL</a></th>
<th scope="col" class="column col-ts is-2 is-hidden-mobile"><a role="button" href="#" @click="${
this.onSort
}" @keyup="${clickOnSpacebarPress}" data-key="ts" class="${
this.sortKey === "ts" ? (this.sortDesc ? "desc" : "asc") : ""
}">Date</a></th>
<th scope="col" class="column col-mime is-3 is-hidden-mobile"><a role="button" href="#" @click="${
this.onSort
}" @keyup="${clickOnSpacebarPress}" data-key="mime" class="${
this.sortKey === "mime" ? (this.sortDesc ? "desc" : "asc") : ""
}">Mime Type</a></th>
<th scope="col" class="column col-status is-1 is-hidden-mobile"><a role="button" href="#" @click="${
this.onSort
}" @keyup="${clickOnSpacebarPress}" data-key="status" class="${
this.sortKey === "status" ? (this.sortDesc ? "desc" : "asc") : ""
}">Status</a></th>
</tr>
</thead>
<tbody class="main-scroll" @scroll="${this.onScroll}">
${this.sortedResults.length ?
this.sortedResults.map((result) => html`
${
this.sortedResults.length
? this.sortedResults.map(
(result) => html`
<tr class="columns result">
<td class="column col-url is-6"><p class="minihead is-hidden-tablet">URL</p><a @click="${this.onReplay}" data-url="${result.url}" data-ts="${result.ts}" href="${getReplayLink("resources", result.url, result.ts)}">
<keyword-mark keywords="${this.query}">${result.url}</keyword-mark>
</a></td>
<td class="column col-ts is-2"><p class="minihead is-hidden-tablet">Date</p>${new Date(result.date).toLocaleString()}</td>
<td class="column col-mime is-3"><p class="minihead is-hidden-tablet">Mime Type</p>${result.mime}</td>
<td class="column col-status is-1"><p class="minihead is-hidden-tablet">Status</p>${result.status}</td>
<td class="column col-url is-6">
<p class="minihead is-hidden-tablet">URL</p>
<a
@click="${this.onReplay}"
data-url="${result.url}"
data-ts="${result.ts}"
href="${getReplayLink(
"resources",
result.url,
result.ts,
)}"
>
<keyword-mark keywords="${this.query}"
>${result.url}</keyword-mark
>
</a>
</td>
<td class="column col-ts is-2">
<p class="minihead is-hidden-tablet">Date</p>
${new Date(result.date).toLocaleString()}
</td>
<td class="column col-mime is-3">
<p class="minihead is-hidden-tablet">Mime Type</p>
${result.mime}
</td>
<td class="column col-status is-1">
<p class="minihead is-hidden-tablet">Status</p>
${result.status}
</td>
</tr>
`) : html`<tr class="section"><td colspan="4"><i>No Results Found.</i></td></tr>`}
`,
)
: html`<tr class="section">
<td colspan="4"><i>No Results Found.</i></td>
</tr>`
}
</tbody>
</table>
`;
+10
View File
@@ -2209,6 +2209,11 @@ escape-string-regexp@^4.0.0:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f"
integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==
eslint-scope@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
@@ -4491,6 +4496,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.2.tgz#78fcecd6d870551aa5547437cdae39d4701dca5b"
integrity sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==
pretty-bytes@^5.6.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"