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:
+4
-13
@@ -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"
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Ignore everything except src
|
||||
*
|
||||
!/src/
|
||||
!src/*
|
||||
+4
-1
@@ -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",
|
||||
|
||||
+325
-195
@@ -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();
|
||||
@@ -104,105 +108,104 @@ class ReplayWebApp extends LitElement
|
||||
|
||||
static get appStyles() {
|
||||
return css`
|
||||
#wrlogo {
|
||||
max-height: 1.0rem;
|
||||
}
|
||||
.navbar {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.navbar-brand {
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wr-logo-item {
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.has-allcaps {
|
||||
font-variant-caps: small-caps;
|
||||
}
|
||||
:host {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
min-width: 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
wr-coll {
|
||||
height: 100%;
|
||||
}
|
||||
.navbar {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
div.navbar-menu fa-icon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
.tagline {
|
||||
margin-top: 1.0rem;
|
||||
}
|
||||
|
||||
.drop-file-overlay {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
background: rgba(255,255,255,.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.drop-file-overlay:after {
|
||||
pointer-events: none;
|
||||
content: " ";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 5px dashed #aaa;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 840px) {
|
||||
.menu-only {
|
||||
display: none;
|
||||
#wrlogo {
|
||||
max-height: 1rem;
|
||||
}
|
||||
.navbar {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.navbar-brand {
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.wr-logo-item {
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.has-allcaps {
|
||||
font-variant-caps: small-caps;
|
||||
}
|
||||
:host {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
min-width: 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
wr-coll {
|
||||
height: 100%;
|
||||
}
|
||||
.navbar {
|
||||
padding: 0 0.5em;
|
||||
}
|
||||
|
||||
a.arrow-button {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
div.navbar-menu fa-icon {
|
||||
vertical-align: sub;
|
||||
}
|
||||
.tagline {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-menu {
|
||||
padding: 0 1.0em;
|
||||
.drop-file-overlay {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 50;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
padding-left: 0px;
|
||||
margin-left: 6px;
|
||||
.drop-file-overlay:after {
|
||||
pointer-events: none;
|
||||
content: " ";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 5px dashed #aaa;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
a.navbar-item.logo-text:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 840px) {
|
||||
.menu-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.wide-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
a.arrow-button {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.info-menu {
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
padding-left: 0px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
a.navbar-item.logo-text:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 840px) {
|
||||
.wide-only {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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,88 +225,176 @@ class ReplayWebApp extends LitElement
|
||||
}
|
||||
|
||||
renderNavBar() {
|
||||
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.renderNavBrand()}
|
||||
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.renderNavBrand()}
|
||||
</a>
|
||||
${this.collTitle
|
||||
? html`
|
||||
<a
|
||||
href="${this.collPageUrl}"
|
||||
class="no-wrap is-size-6 has-text-black"
|
||||
>/ <i>${this.collTitle}</i></a
|
||||
>
|
||||
<span class="no-wrap is-size-6"
|
||||
> /
|
||||
${this.pageReplay
|
||||
? html`<i>${this.pageTitle}</i>`
|
||||
: this.pageTitle}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`
|
||||
<span class="navbar-item wr-logo-item">
|
||||
<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}"
|
||||
>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
${this.collTitle ? html`
|
||||
<a href="${this.collPageUrl}" class="no-wrap is-size-6 has-text-black">/ <i>${this.collTitle}</i></a>
|
||||
<span class="no-wrap is-size-6"> /
|
||||
${this.pageReplay ? html`<i>${this.pageTitle}</i>` : this.pageTitle}
|
||||
</span>
|
||||
` : ""}
|
||||
`: html`
|
||||
<span class="navbar-item wr-logo-item">
|
||||
<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}">
|
||||
<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" : ""}">
|
||||
<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"> 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"> Go Forward</span>
|
||||
</a>
|
||||
` : ""}
|
||||
</div>
|
||||
${!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>`;
|
||||
${!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"> 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"
|
||||
> Go Forward</span
|
||||
>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${!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> User Docs</span>
|
||||
</a>
|
||||
<!--
|
||||
return html` <a href="/docs" target="_blank" class="navbar-item is-size-6">
|
||||
<fa-icon .svg="${fasHelp}" aria-hidden="true"></fa-icon
|
||||
><span> 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>`;
|
||||
<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}"
|
||||
sourceUrl="${this.sourceUrl}"
|
||||
embed="${this.embed}"
|
||||
appName="${this.appName}"
|
||||
swName="${this.swName}"
|
||||
.appLogo="${this.mainLogo}"
|
||||
@replay-favicons=${this.onFavIcons}
|
||||
@update-title=${this.onTitle}
|
||||
@coll-loaded=${this.onCollLoaded}
|
||||
@about-show=${() => this.showAbout = true}></wr-coll>`;
|
||||
return html` <wr-coll
|
||||
.loadInfo="${this.loadInfo}"
|
||||
sourceUrl="${this.sourceUrl}"
|
||||
embed="${this.embed}"
|
||||
appName="${this.appName}"
|
||||
swName="${this.swName}"
|
||||
.appLogo="${this.mainLogo}"
|
||||
@replay-favicons=${this.onFavIcons}
|
||||
@update-title=${this.onTitle}
|
||||
@coll-loaded=${this.onCollLoaded}
|
||||
@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() : ""}
|
||||
`;
|
||||
}
|
||||
@@ -331,8 +421,13 @@ class ReplayWebApp extends LitElement
|
||||
// service worker name
|
||||
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 = new SWManager({ name, appName: this.appName });
|
||||
this.swmanager
|
||||
.register()
|
||||
.catch(
|
||||
() =>
|
||||
(this.swErrorMsg = this.swmanager.renderErrorReport(this.mainLogo)),
|
||||
);
|
||||
|
||||
window.addEventListener("popstate", () => {
|
||||
this.initRoute();
|
||||
@@ -361,7 +456,7 @@ class ReplayWebApp extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
skipMenu(event){
|
||||
skipMenu(event) {
|
||||
// This is a workaround, since this app's routing doesn't permit normal
|
||||
// following of in-page anchors.
|
||||
event.preventDefault();
|
||||
@@ -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) => {
|
||||
event.preventDefault();
|
||||
this.navMenuShown = false;
|
||||
this.renderRoot.querySelector("#menu-button").focus();
|
||||
}, {once: true});
|
||||
document.addEventListener("keypress", (event) => {
|
||||
if (event.key == "Escape") {
|
||||
document.addEventListener(
|
||||
"click",
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
this.navMenuShown = false;
|
||||
this.renderRoot.querySelector("#menu-button").focus();
|
||||
}
|
||||
}, {once: true});
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
document.addEventListener(
|
||||
"keypress",
|
||||
(event) => {
|
||||
if (event.key == "Escape") {
|
||||
event.preventDefault();
|
||||
this.navMenuShown = false;
|
||||
this.renderRoot.querySelector("#menu-button").focus();
|
||||
}
|
||||
},
|
||||
{ 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>
|
||||
|
||||
+131
-86
@@ -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) {
|
||||
@@ -97,7 +107,7 @@ class Chooser extends LitElement
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
const loadInfo = {sourceUrl: this.fileDisplayName};
|
||||
const loadInfo = { sourceUrl: this.fileDisplayName };
|
||||
|
||||
if (this.file) {
|
||||
loadInfo.isFile = true;
|
||||
@@ -108,7 +118,7 @@ class Chooser extends LitElement
|
||||
loadInfo.noCache = true;
|
||||
} else if (this.fileHandle) {
|
||||
loadInfo.loadUrl = this.fileDisplayName;
|
||||
loadInfo.extra = {fileHandle: this.fileHandle};
|
||||
loadInfo.extra = { fileHandle: this.fileHandle };
|
||||
loadInfo.noCache = false;
|
||||
} else {
|
||||
loadInfo.loadUrl = URL.createObjectURL(this.file);
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -137,97 +157,122 @@ class Chooser extends LitElement
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
min-width: 0;
|
||||
}
|
||||
.extra-padding {
|
||||
padding: 1.5em;
|
||||
}
|
||||
.less-padding {
|
||||
padding-top: 1.0em;
|
||||
padding-bottom: 1.0em;
|
||||
}
|
||||
div.field.has-addons {
|
||||
flex: auto;
|
||||
}
|
||||
.panel-heading {
|
||||
background-color: #cff3ff;
|
||||
}
|
||||
.message-header {
|
||||
background-color: #cff3ff;
|
||||
color: black;
|
||||
}
|
||||
.heading-size {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
form {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
p.control.is-expanded {
|
||||
width: min-content;
|
||||
}
|
||||
input.input.file-name:invalid {
|
||||
border: 1px dashed red;
|
||||
}
|
||||
input.input.file-name {
|
||||
border-width: 1px;
|
||||
margin-left: -1px;
|
||||
max-width: 100%;
|
||||
}
|
||||
@media screen and (max-width: 1023px) {
|
||||
.file-icon {
|
||||
margin-right: 0px;
|
||||
:host {
|
||||
min-width: 0;
|
||||
}
|
||||
.extra-padding {
|
||||
padding: 1.5em;
|
||||
}
|
||||
.less-padding {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
div.field.has-addons {
|
||||
flex: auto;
|
||||
}
|
||||
.panel-heading {
|
||||
background-color: #cff3ff;
|
||||
}
|
||||
.message-header {
|
||||
background-color: #cff3ff;
|
||||
color: black;
|
||||
}
|
||||
.heading-size {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
form {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
p.control.is-expanded {
|
||||
width: min-content;
|
||||
}
|
||||
input.input.file-name:invalid {
|
||||
border: 1px dashed red;
|
||||
}
|
||||
input.input.file-name {
|
||||
border-width: 1px;
|
||||
margin-left: -1px;
|
||||
max-width: 100%;
|
||||
}
|
||||
@media screen and (max-width: 1023px) {
|
||||
.file-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
#filename {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
@media screen and (max-width: 768px) {
|
||||
#filename {
|
||||
border-bottom-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
`);
|
||||
}
|
||||
|
||||
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:\/\/.+)"
|
||||
.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"}">
|
||||
<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"}"
|
||||
/>
|
||||
</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>
|
||||
|
||||
+341
-274
@@ -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 ||
|
||||
coll.filename.indexOf(this.query) >= 0 ||
|
||||
(coll.loadUrl && coll.loadUrl.indexOf(this.query) >= 0) ||
|
||||
(coll.title && coll.title.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)
|
||||
) {
|
||||
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);
|
||||
@@ -140,7 +135,7 @@ class CollIndex extends LitElement
|
||||
this._deleting[coll.sourceUrl] = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const resp = await fetch(`${apiPrefix}/c/${coll.id}`, {method: "DELETE"});
|
||||
const resp = await fetch(`${apiPrefix}/c/${coll.id}`, { method: "DELETE" });
|
||||
if (resp.status === 200) {
|
||||
const json = await resp.json();
|
||||
this.colls = json.colls;
|
||||
@@ -154,106 +149,108 @@ class CollIndex extends LitElement
|
||||
|
||||
static get compStyles() {
|
||||
return css`
|
||||
:host {
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.size {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.extra-padding {
|
||||
padding: 2em;
|
||||
}
|
||||
.no-top-padding {
|
||||
padding-top: 1.0em;
|
||||
}
|
||||
.panel-heading {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.is-loading {
|
||||
line-height: 1.5em;
|
||||
height: 1.5em;
|
||||
border: 0px;
|
||||
background-color: transparent !important;
|
||||
width: auto;
|
||||
}
|
||||
div.panel.is-light {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
:host {
|
||||
overflow-y: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.size {
|
||||
margin-right: 20px;
|
||||
}
|
||||
.extra-padding {
|
||||
padding: 2em;
|
||||
}
|
||||
.no-top-padding {
|
||||
padding-top: 1em;
|
||||
}
|
||||
.panel-heading {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.is-loading {
|
||||
line-height: 1.5em;
|
||||
height: 1.5em;
|
||||
border: 0px;
|
||||
background-color: transparent !important;
|
||||
width: auto;
|
||||
}
|
||||
div.panel.is-light {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
fa-icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.panel-color {
|
||||
background-color: rgb(210, 249, 214);
|
||||
}
|
||||
.panel-color {
|
||||
background-color: rgb(210, 249, 214);
|
||||
}
|
||||
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.sort-header {
|
||||
padding: 0.3rem 0.3rem 0.3rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.sort-header .control {
|
||||
flex: auto;
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.sort-header {
|
||||
padding: 0.3rem 0.3rem 0.3rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.sort-header .control {
|
||||
flex: auto;
|
||||
|
||||
padding-left: 0.3rem;
|
||||
width: initial;
|
||||
}
|
||||
wr-sorter {
|
||||
padding: 0.3rem;
|
||||
}
|
||||
a.button.is-small.collapse {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.icon.is-left {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.coll-block {
|
||||
position: relative;
|
||||
}
|
||||
.delete-button {
|
||||
width: 32px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
#sort-select::after {
|
||||
display: none;
|
||||
}
|
||||
header {
|
||||
transform: translate(0px, 0px);
|
||||
transition: all 0.5s ease 0s;
|
||||
visibility: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
header.closed {
|
||||
transform: translate(0, -100%);
|
||||
transition: all 0.5s ease 0s;
|
||||
visibility: visible;
|
||||
height: 269px;
|
||||
margin-top: -269px;
|
||||
}
|
||||
padding-left: 0.3rem;
|
||||
width: initial;
|
||||
}
|
||||
wr-sorter {
|
||||
padding: 0.3rem;
|
||||
}
|
||||
a.button.is-small.collapse {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.icon.is-left {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.coll-block {
|
||||
position: relative;
|
||||
}
|
||||
.delete-button {
|
||||
width: 32px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
#sort-select::after {
|
||||
display: none;
|
||||
}
|
||||
header {
|
||||
transform: translate(0px, 0px);
|
||||
transition: all 0.5s ease 0s;
|
||||
visibility: visible;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
header.closed {
|
||||
transform: translate(0, -100%);
|
||||
transition: all 0.5s ease 0s;
|
||||
visibility: visible;
|
||||
height: 269px;
|
||||
margin-top: -269px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -264,57 +261,88 @@ class CollIndex extends LitElement
|
||||
const hasHeader = this.childElementCount > 0;
|
||||
|
||||
return html`
|
||||
<header class="${this.hideHeader ? "closed" : ""}">
|
||||
<slot name="header"></slot>
|
||||
</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>` : ""}
|
||||
</div>
|
||||
<div class="panel">
|
||||
${this.renderHeader()}
|
||||
${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>
|
||||
</div>
|
||||
<wr-sorter id="index"
|
||||
sortKey="ctime"
|
||||
?sortDesc="${true}"
|
||||
.sortKeys="${this.sortKeys}"
|
||||
.data="${this.filteredColls}"
|
||||
@sort-changed="${(e) => this.sortedColls = e.detail.sortedData}">
|
||||
</wr-sorter>
|
||||
<header class="${this.hideHeader ? "closed" : ""}">
|
||||
<slot name="header"></slot>
|
||||
</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>`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="panel">
|
||||
${this.renderHeader()}
|
||||
${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>
|
||||
</div>
|
||||
<wr-sorter
|
||||
id="index"
|
||||
sortKey="ctime"
|
||||
?sortDesc="${true}"
|
||||
.sortKeys="${this.sortKeys}"
|
||||
.data="${this.filteredColls}"
|
||||
@sort-changed="${(e) =>
|
||||
(this.sortedColls = e.detail.sortedData)}"
|
||||
>
|
||||
</wr-sorter>
|
||||
</div>
|
||||
|
||||
<div class="coll-list">
|
||||
${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`
|
||||
<div class="coll-list">
|
||||
${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`
|
||||
<span class="button delete-button is-loading is-static">Deleting</span`}
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="panel-block extra-padding">
|
||||
${this.sortedColls === null
|
||||
? html`<i>Loading Archives...</i>`
|
||||
: this.renderEmpty()}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
` : html`
|
||||
|
||||
<div class="panel-block extra-padding">
|
||||
${this.sortedColls === null ? html`<i>Loading Archives...</i>` :
|
||||
this.renderEmpty()}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -351,85 +380,99 @@ class CollInfo extends LitElement
|
||||
|
||||
static get compStyles() {
|
||||
return css`
|
||||
.columns {
|
||||
width: 100%;
|
||||
}
|
||||
.column {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
.columns {
|
||||
width: 100%;
|
||||
}
|
||||
.column {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0px;
|
||||
}
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
:host(.is-list) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host(.is-list) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host(.is-list) .column {
|
||||
width: 100% !important;
|
||||
}
|
||||
:host(.is-list) .column {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.col-title:hover {
|
||||
|
||||
}
|
||||
.col-title a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.column:hover > .copy, .source-text:hover + .copy, .copy:hover {
|
||||
display: inline;
|
||||
}
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.col-title:hover {
|
||||
}
|
||||
.col-title a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.column:hover > .copy,
|
||||
.source-text:hover + .copy,
|
||||
.copy:hover {
|
||||
display: inline;
|
||||
}
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
}
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
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} </a>` : html`
|
||||
${coll.sourceUrl} `}
|
||||
</span>
|
||||
<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} </a>`
|
||||
: html` ${coll.sourceUrl} `}
|
||||
</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>` : ""}
|
||||
</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>
|
||||
`;
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
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,67 +482,91 @@ 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">
|
||||
<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">
|
||||
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">
|
||||
<p class="minihead">Description</p>
|
||||
${coll.desc}
|
||||
</div>` : html`
|
||||
`}
|
||||
<div class="column"><p class="minihead">Filename</p>${coll.filename}</div>
|
||||
${this.renderSource(coll)}
|
||||
</div>`
|
||||
: html``}
|
||||
<div class="column">
|
||||
<p class="minihead">Filename</p>
|
||||
${coll.filename}
|
||||
</div>
|
||||
${this.renderSource(coll)}
|
||||
${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
|
||||
>`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: software
|
||||
? html`
|
||||
<div class="column">
|
||||
<p class="minihead">Created With</p>
|
||||
${software || "Unknown"}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
${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>` : ""}
|
||||
</div>
|
||||
` : 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`}
|
||||
</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`}
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Package Hash</p>
|
||||
<div class="column">
|
||||
<p class="minihead">Package Hash</p>
|
||||
${datapackageHash || "Not Available"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Observer Public Key</p>
|
||||
<div class="column">
|
||||
<p class="minihead">Observer Public Key</p>
|
||||
${publicKey || "Not Available"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Loading Mode</p>
|
||||
${coll.onDemand ? "Download On-Demand" : "Fully Local"}
|
||||
</div>
|
||||
<div class="column">
|
||||
<p class="minihead">Collection id</p>
|
||||
${coll.coll}
|
||||
</div>
|
||||
</div>`;
|
||||
<div class="column">
|
||||
<p class="minihead">Loading Mode</p>
|
||||
${coll.onDemand ? "Download On-Demand" : "Fully Local"}
|
||||
</div>
|
||||
<div class="column">
|
||||
<p class="minihead">Collection id</p>
|
||||
${coll.coll}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -514,8 +581,8 @@ class CollInfo extends LitElement
|
||||
}
|
||||
|
||||
onPurge(reload) {
|
||||
const detail = {reload};
|
||||
this.dispatchEvent(new CustomEvent("coll-purge", {detail}));
|
||||
const detail = { reload };
|
||||
this.dispatchEvent(new CustomEvent("coll-purge", { detail }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+896
-507
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -27,9 +27,9 @@ async function getDB(name) {
|
||||
async function getResponse(event, request, coll, ts, channel) {
|
||||
const db = await getDB(coll);
|
||||
|
||||
const req = {request, url: request.url, timestamp: ts};
|
||||
const req = { request, url: request.url, timestamp: ts };
|
||||
|
||||
const result = await db.getResource(req, "", {request});
|
||||
const result = await db.getResource(req, "", { request });
|
||||
|
||||
if (!result) {
|
||||
ipcRenderer.send(channel, 404);
|
||||
@@ -46,7 +46,3 @@ async function getResponse(event, request, coll, ts, channel) {
|
||||
ipcRenderer.on("getresponse", getResponse);
|
||||
|
||||
export { getDB, getColl, loader };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+110
-56
@@ -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,9 +38,8 @@ const FILE_PROTO = "file2";
|
||||
const URL_RX = /([^/]+)\/([\d]+)(?:\w\w_)?\/(.*)$/;
|
||||
|
||||
// ============================================================================
|
||||
class ElectronReplayApp
|
||||
{
|
||||
constructor({staticPath="./", profileName=""} = {}) {
|
||||
class ElectronReplayApp {
|
||||
constructor({ staticPath = "./", profileName = "" } = {}) {
|
||||
this.pluginPath = "";
|
||||
|
||||
this.appPath = app.getAppPath();
|
||||
@@ -50,7 +57,7 @@ class ElectronReplayApp
|
||||
|
||||
this.openNextFile = null;
|
||||
|
||||
this.screenSize = {width: 1024, height: 768};
|
||||
this.screenSize = { width: 1024, height: 768 };
|
||||
|
||||
this.origUA = null;
|
||||
}
|
||||
@@ -76,10 +83,12 @@ 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*/) => {
|
||||
app.on("second-instance", (event, commandLine /*, workingDir*/) => {
|
||||
// Just create a new window in case of second instance request
|
||||
this.createMainWindow(commandLine);
|
||||
});
|
||||
@@ -93,21 +102,26 @@ 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([{
|
||||
scheme: FILE_PROTO,
|
||||
privileges: {
|
||||
standard: false,
|
||||
secure: true,
|
||||
bypassCSP: true,
|
||||
allowServiceWorkers: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
stream: true
|
||||
}
|
||||
}]);
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: FILE_PROTO,
|
||||
privileges: {
|
||||
standard: false,
|
||||
secure: true,
|
||||
bypassCSP: true,
|
||||
allowServiceWorkers: true,
|
||||
supportFetchAPI: true,
|
||||
corsEnabled: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
app.on("will-finish-launching", () => {
|
||||
app.on("open-file", (event, filePath) => {
|
||||
@@ -149,28 +163,32 @@ class ElectronReplayApp
|
||||
this.screenSize = screen.getPrimaryDisplay().workAreaSize;
|
||||
|
||||
app.on("web-contents-created", (event, contents) => {
|
||||
contents.setWindowOpenHandler(({url}) => {
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
// load docs in native browser for now
|
||||
if (url === STATIC_PREFIX + "docs") {
|
||||
shell.openExternal("https://replayweb.page/docs/");
|
||||
return { action: "deny"};
|
||||
return { action: "deny" };
|
||||
}
|
||||
|
||||
// load external URLs in native browser
|
||||
if (!url.startsWith(STATIC_PREFIX)) {
|
||||
shell.openExternal(url);
|
||||
return { action: "deny"};
|
||||
return { action: "deny" };
|
||||
}
|
||||
|
||||
return { action: "allow"};
|
||||
return { action: "allow" };
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
@@ -182,13 +200,13 @@ class ElectronReplayApp
|
||||
//const filename = parsedUrl.searchParams.get("filename");
|
||||
|
||||
if (request.url === FILE_PROTO + "://localhost") {
|
||||
callback({statusCode: 200, data: null});
|
||||
callback({ statusCode: 200, data: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const filename = url.fileURLToPath(request.url.replace(FILE_PROTO, "file"));
|
||||
|
||||
const headers = {"Content-Type": "application/octet-stream"};
|
||||
const headers = { "Content-Type": "application/octet-stream" };
|
||||
const reqHeaders = new Headers(request.headers);
|
||||
|
||||
if (filename) {
|
||||
@@ -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});
|
||||
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,10 +284,14 @@ 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return this.notFound(request.url, callback);
|
||||
}
|
||||
|
||||
@@ -278,9 +305,11 @@ class ElectronReplayApp
|
||||
|
||||
async proxyLive(request, callback) {
|
||||
let headers = request.headers;
|
||||
const {method, url, uploadData} = request;
|
||||
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
|
||||
@@ -290,23 +319,29 @@ class ElectronReplayApp
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(url, {method, headers, body});
|
||||
response = await fetch(url, { method, headers, body });
|
||||
} catch (e) {
|
||||
console.warn("fetch failed for: " + url);
|
||||
callback({statusCode: 502, headers: {}, data: null});
|
||||
callback({ statusCode: 502, headers: {}, data: null });
|
||||
return;
|
||||
}
|
||||
const data = method === "HEAD" ? null : response.body;
|
||||
const statusCode = response.status;
|
||||
|
||||
headers = Object.fromEntries(response.headers.entries());
|
||||
callback({statusCode, headers, data});
|
||||
callback({ statusCode, headers, data });
|
||||
}
|
||||
|
||||
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});
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -353,16 +398,21 @@ class ElectronReplayApp
|
||||
}
|
||||
|
||||
data = this._bufferToStream(data);
|
||||
|
||||
callback({statusCode: status, headers, data});
|
||||
|
||||
callback({ statusCode: status, headers, data });
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,12 +424,12 @@ class ElectronReplayApp
|
||||
if (headers) {
|
||||
headers["Content-Length"] = "" + size;
|
||||
}
|
||||
return {statusCode};
|
||||
return { statusCode };
|
||||
}
|
||||
|
||||
const m = range.match(/bytes=([\d]+)-([\d]*)/);
|
||||
if (!m) {
|
||||
return {statusCode};
|
||||
return { statusCode };
|
||||
}
|
||||
|
||||
const start = Number(m[1]);
|
||||
@@ -389,7 +439,7 @@ class ElectronReplayApp
|
||||
headers["Content-Range"] = `bytes ${start}-${end}/${size}`;
|
||||
headers["Content-Length"] = `${end - start + 1}`;
|
||||
}
|
||||
return {statusCode, start, end};
|
||||
return { statusCode, start, end };
|
||||
}
|
||||
|
||||
createMainWindow(argv) {
|
||||
@@ -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 = "";
|
||||
@@ -438,7 +492,7 @@ class ElectronReplayApp
|
||||
urlParams.set("url", openUrl);
|
||||
}
|
||||
|
||||
if (openTS) {
|
||||
if (openTS) {
|
||||
urlParams.set("ts", openTS);
|
||||
}
|
||||
|
||||
@@ -451,7 +505,7 @@ class ElectronReplayApp
|
||||
}
|
||||
}
|
||||
|
||||
async function * readBody (body, session) {
|
||||
async function* readBody(body, session) {
|
||||
for (const chunk of body) {
|
||||
if (chunk.bytes) {
|
||||
yield await Promise.resolve(chunk.bytes);
|
||||
|
||||
+184
-154
@@ -1,20 +1,17 @@
|
||||
|
||||
import btGlobe from "../assets/globe.svg";
|
||||
import btAngleDoubleDown from "../assets/chevron-double-down.svg";
|
||||
import btAngleDoubleUp from "../assets/chevron-double-up.svg";
|
||||
import fabGithub from "@fortawesome/fontawesome-free/svgs/brands/github.svg";
|
||||
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
|
||||
|
||||
import {clickOnSpacebarPress} from "./misc";
|
||||
import { clickOnSpacebarPress } from "./misc";
|
||||
|
||||
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;
|
||||
@@ -30,180 +27,195 @@ export class RWPEmbedReceipt extends LitElement
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
collInfo: {type: Object},
|
||||
appLogo: {type: Object},
|
||||
url: {type: String},
|
||||
ts: {type: String},
|
||||
collInfo: { type: Object },
|
||||
appLogo: { type: Object },
|
||||
url: { type: String },
|
||||
ts: { type: String },
|
||||
|
||||
active: {type: Boolean }
|
||||
active: { type: Boolean },
|
||||
};
|
||||
}
|
||||
|
||||
static get embedStyles() {
|
||||
return css`
|
||||
rwp-embed-receipt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
rwp-embed-receipt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
.icon {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
#embed-dropdown {
|
||||
max-height: calc(100vh - 50px);
|
||||
padding-top: 0;
|
||||
margin-top: -0.5rem;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
transition: all .3s linear;
|
||||
transform-origin: left top;
|
||||
transform: scaleY(0);
|
||||
transition: all 300ms cubic-bezier(0.15, 0, 0.1, 1);
|
||||
filter: drop-shadow(0px 8px 4px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
#embed-dropdown {
|
||||
max-height: calc(100vh - 50px);
|
||||
padding-top: 0;
|
||||
margin-top: -0.5rem;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
transition: all 0.3s linear;
|
||||
transform-origin: left top;
|
||||
transform: scaleY(0);
|
||||
transition: all 300ms cubic-bezier(0.15, 0, 0.1, 1);
|
||||
filter: drop-shadow(0px 8px 4px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.dropdown.is-active #embed-dropdown {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
.dropdown.is-active #embed-dropdown {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
.embed-info-container {
|
||||
width: 100%;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
}
|
||||
.embed-info-container {
|
||||
width: 100%;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button.embed-info {
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
justify-content: space-between;
|
||||
max-width: 40rem;
|
||||
width: calc(100% - 1rem);
|
||||
height: 42px;
|
||||
border-color: #D1D5DA;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.15));
|
||||
transition-duration: 50ms;
|
||||
transition-timing-function: ease-out;
|
||||
cursor: pointer;
|
||||
z-index: 2
|
||||
}
|
||||
button.embed-info {
|
||||
padding: 0;
|
||||
background-color: white;
|
||||
justify-content: space-between;
|
||||
max-width: 40rem;
|
||||
width: calc(100% - 1rem);
|
||||
height: 42px;
|
||||
border-color: #d1d5da;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.15));
|
||||
transition-duration: 50ms;
|
||||
transition-timing-function: ease-out;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
button.embed-info:active {
|
||||
color: initial;
|
||||
}
|
||||
button.embed-info:active {
|
||||
color: initial;
|
||||
}
|
||||
|
||||
button.embed-info:hover {
|
||||
filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.2));
|
||||
transform: scale(1.01);
|
||||
}
|
||||
button.embed-info:hover {
|
||||
filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.2));
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
button.embed-info:hover:active {
|
||||
transform: translateY(0.25rem);
|
||||
}
|
||||
button.embed-info:hover:active {
|
||||
transform: translateY(0.25rem);
|
||||
}
|
||||
|
||||
.embed-info-buttontext {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
text-align: start;
|
||||
font-size: 13px;
|
||||
}
|
||||
.embed-info-buttontext {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-grow: 1;
|
||||
text-align: start;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.embed-info-drop {
|
||||
font-size: 14px;
|
||||
padding: 1rem;
|
||||
padding-top: 2rem;
|
||||
max-width: 38rem;
|
||||
max-height: 42rem;
|
||||
width: calc(100% - 2rem);
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
pointer-events: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.embed-info-drop {
|
||||
font-size: 14px;
|
||||
padding: 1rem;
|
||||
padding-top: 2rem;
|
||||
max-width: 38rem;
|
||||
max-height: 42rem;
|
||||
width: calc(100% - 2rem);
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
pointer-events: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.embed-info-drop > p {
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
}
|
||||
.embed-info-drop > p {
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.embed-info-drop > h2 {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
color: #24292E;
|
||||
}
|
||||
.embed-info-drop > h2 {
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
color: #24292e;
|
||||
}
|
||||
|
||||
.embed-info-drop-statscontainer > h3 {
|
||||
font-size: 12px;
|
||||
color: #394146;
|
||||
}
|
||||
.embed-info-drop-statscontainer > h3 {
|
||||
font-size: 12px;
|
||||
color: #394146;
|
||||
}
|
||||
|
||||
.embed-info-drop-statscontainer > p {
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
}
|
||||
.embed-info-drop-statscontainer > p {
|
||||
font-size: 14px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.embed-info-drop a {
|
||||
word-break: break-all;
|
||||
}
|
||||
.embed-info-drop a {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.embed-info-drop .show-hash {
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
}
|
||||
.embed-info-drop .show-hash {
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.embed-info-drop .show-key {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
whitespace: nowrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
.embed-info-drop .show-key {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
whitespace: nowrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.embed-globe {
|
||||
margin: 0.25rem;
|
||||
padding: 7px;
|
||||
background-color: #0366D6;
|
||||
border-radius: 9999px;
|
||||
color: white;
|
||||
border-width: 1px;
|
||||
border-color: #D1D5DA;
|
||||
border-style: solid;
|
||||
line-height: 0.5em;
|
||||
}`;
|
||||
.embed-globe {
|
||||
margin: 0.25rem;
|
||||
padding: 7px;
|
||||
background-color: #0366d6;
|
||||
border-radius: 9999px;
|
||||
color: white;
|
||||
border-width: 1px;
|
||||
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`
|
||||
<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>` : ""}
|
||||
${
|
||||
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>`
|
||||
: ""
|
||||
}
|
||||
<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>
|
||||
|
||||
+49
-29
@@ -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/";
|
||||
@@ -86,7 +83,7 @@ class Embed extends LitElement
|
||||
|
||||
errorMessage: { type: String },
|
||||
|
||||
requireSubdomainIframe: {type: Boolean},
|
||||
requireSubdomainIframe: { type: Boolean },
|
||||
|
||||
loading: { type: String },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -196,7 +203,7 @@ class Embed extends LitElement
|
||||
}
|
||||
|
||||
if (this.config) {
|
||||
const config = {...this.customConfig, ...JSON.parse(this.config)};
|
||||
const config = { ...this.customConfig, ...JSON.parse(this.config) };
|
||||
return JSON.stringify(config);
|
||||
} else {
|
||||
return JSON.stringify(this.customConfig);
|
||||
@@ -204,13 +211,14 @@ class Embed extends LitElement
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("url") ||
|
||||
changedProperties.has("ts") ||
|
||||
changedProperties.has("query") ||
|
||||
changedProperties.has("view") ||
|
||||
changedProperties.has("source") ||
|
||||
changedProperties.has("src")) {
|
||||
|
||||
if (
|
||||
changedProperties.has("url") ||
|
||||
changedProperties.has("ts") ||
|
||||
changedProperties.has("query") ||
|
||||
changedProperties.has("view") ||
|
||||
changedProperties.has("source") ||
|
||||
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,18 +308,26 @@ 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``}
|
||||
|
||||
${this.errorMessage}
|
||||
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;
|
||||
|
||||
+63
-40
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,22 +63,25 @@ class GDrive extends LitElement
|
||||
try {
|
||||
const abort = new AbortController();
|
||||
const signal = abort.signal;
|
||||
resp = await fetch(publicUrl, {signal});
|
||||
resp = await fetch(publicUrl, { signal });
|
||||
abort.abort();
|
||||
if (resp.status != 200) {
|
||||
return false;
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const name = json.name;
|
||||
const extra = {publicUrl};
|
||||
const extra = { publicUrl };
|
||||
const size = Number(json.size);
|
||||
|
||||
this.dispatchEvent(new CustomEvent("load-ready", {detail: {name, extra, size, sourceUrl}}));
|
||||
return true;
|
||||
|
||||
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,24 +146,36 @@ class GDrive extends LitElement
|
||||
}
|
||||
|
||||
render() {
|
||||
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>
|
||||
</div>
|
||||
<br/>
|
||||
` : ""}
|
||||
<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>
|
||||
`}`;
|
||||
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>
|
||||
</div>
|
||||
<br />
|
||||
`
|
||||
: ""}
|
||||
<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>
|
||||
`}`;
|
||||
}
|
||||
|
||||
script() {
|
||||
@@ -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({
|
||||
// eslint-disable-next-line no-undef
|
||||
client_id: __GDRIVE_CLIENT_ID__,
|
||||
scope: "https://www.googleapis.com/auth/drive.file",
|
||||
response_type: "token",
|
||||
prompt
|
||||
}, callback);
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+159
-122
@@ -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;
|
||||
@@ -41,7 +39,7 @@ class Loader extends LitElement
|
||||
percent: { type: Number },
|
||||
currentSize: { type: Number },
|
||||
totalSize: { type: Number },
|
||||
error: { type: String},
|
||||
error: { type: String },
|
||||
total: { type: Number },
|
||||
status: { type: String },
|
||||
coll: { type: String },
|
||||
@@ -74,48 +72,53 @@ class Loader extends LitElement
|
||||
|
||||
this.worker.addEventListener("message", (event) => {
|
||||
switch (event.data.msg_type) {
|
||||
case "collProgress":
|
||||
if (event.data.name === this.coll) {
|
||||
this.percent = event.data.percent;
|
||||
if (event.data.error) {
|
||||
this.error = event.data.error;
|
||||
this.state = "errored";
|
||||
this.errorAllowRetry = true;
|
||||
this.fileHandle = event.data.fileHandle;
|
||||
if (this.error === "missing_local_file") {
|
||||
this.tryFileHandle = false;
|
||||
} else if (this.error === "permission_needed" && event.data.fileHandle) {
|
||||
this.state = "permission_needed";
|
||||
break;
|
||||
case "collProgress":
|
||||
if (event.data.name === this.coll) {
|
||||
this.percent = event.data.percent;
|
||||
if (event.data.error) {
|
||||
this.error = event.data.error;
|
||||
this.state = "errored";
|
||||
this.errorAllowRetry = true;
|
||||
this.fileHandle = event.data.fileHandle;
|
||||
if (this.error === "missing_local_file") {
|
||||
this.tryFileHandle = false;
|
||||
} else if (
|
||||
this.error === "permission_needed" &&
|
||||
event.data.fileHandle
|
||||
) {
|
||||
this.state = "permission_needed";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (event.data.currentSize && event.data.totalSize) {
|
||||
this.currentSize = event.data.currentSize;
|
||||
this.totalSize = event.data.totalSize;
|
||||
}
|
||||
this.extraMsg = event.data.extraMsg;
|
||||
}
|
||||
break;
|
||||
|
||||
case "collAdded":
|
||||
if (event.data.name === this.coll) {
|
||||
if (!this.total) {
|
||||
this.total = 100;
|
||||
}
|
||||
this.progress = this.total;
|
||||
this.percent = 100;
|
||||
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: event.data}));
|
||||
|
||||
if (!this.noWebWorker) {
|
||||
this.worker.terminate();
|
||||
} else {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
if (event.data.currentSize && event.data.totalSize) {
|
||||
this.currentSize = event.data.currentSize;
|
||||
this.totalSize = event.data.totalSize;
|
||||
}
|
||||
this.extraMsg = event.data.extraMsg;
|
||||
}
|
||||
this.worker = null;
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case "collAdded":
|
||||
if (event.data.name === this.coll) {
|
||||
if (!this.total) {
|
||||
this.total = 100;
|
||||
}
|
||||
this.progress = this.total;
|
||||
this.percent = 100;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("coll-loaded", { detail: event.data }),
|
||||
);
|
||||
|
||||
if (!this.noWebWorker) {
|
||||
this.worker.terminate();
|
||||
} else {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
}
|
||||
}
|
||||
this.worker = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -137,43 +140,45 @@ class Loader extends LitElement
|
||||
|
||||
// custom protocol handlers here...
|
||||
try {
|
||||
const {scheme, host, path} = parseURLSchemeHostPath(sourceUrl);
|
||||
const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl);
|
||||
|
||||
switch (scheme) {
|
||||
case "googledrive":
|
||||
this.state = "googledrive";
|
||||
source = await this.googledriveInit();
|
||||
break;
|
||||
case "googledrive":
|
||||
this.state = "googledrive";
|
||||
source = await this.googledriveInit();
|
||||
break;
|
||||
|
||||
case "s3":
|
||||
source = {sourceUrl,
|
||||
loadUrl: `https://${host}.s3.amazonaws.com${path}`,
|
||||
name: this.sourceUrl};
|
||||
break;
|
||||
case "s3":
|
||||
source = {
|
||||
sourceUrl,
|
||||
loadUrl: `https://${host}.s3.amazonaws.com${path}`,
|
||||
name: this.sourceUrl,
|
||||
};
|
||||
break;
|
||||
|
||||
case "file":
|
||||
if (!this.loadInfo && !this.tryFileHandle) {
|
||||
this.state = "errored";
|
||||
this.error = `\
|
||||
case "file":
|
||||
if (!this.loadInfo && !this.tryFileHandle) {
|
||||
this.state = "errored";
|
||||
this.error = `\
|
||||
File URLs can not be entered directly or shared.
|
||||
You can select a file to upload from the main page by clicking the 'Choose File...' button.`;
|
||||
this.errorAllowRetry = false;
|
||||
return;
|
||||
}
|
||||
this.errorAllowRetry = false;
|
||||
return;
|
||||
}
|
||||
|
||||
source = this.loadInfo;
|
||||
break;
|
||||
source = this.loadInfo;
|
||||
break;
|
||||
|
||||
case "proxy":
|
||||
sourceUrl = "proxy:" + sourceUrl.slice("proxy://".length);
|
||||
break;
|
||||
case "proxy":
|
||||
sourceUrl = "proxy:" + sourceUrl.slice("proxy://".length);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
source = {sourceUrl};
|
||||
source = { sourceUrl };
|
||||
}
|
||||
|
||||
this.state = "started";
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -314,73 +328,96 @@ You can select a file to upload from the main page by clicking the 'Choose File.
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<section class="container">
|
||||
<div class="has-text-centered is-flex">
|
||||
<wr-anim-logo class="logo" size="96px"/>
|
||||
</div>
|
||||
${!this.embed ? html`
|
||||
<div class="level">
|
||||
<p class="level-item">Loading <b>${this.sourceUrl}</b>...</p>
|
||||
</div>` : ""}
|
||||
<div class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
${this.renderContent()}
|
||||
<section class="container">
|
||||
<div class="has-text-centered is-flex">
|
||||
<wr-anim-logo class="logo" size="96px" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
${!this.embed
|
||||
? html` <div class="level">
|
||||
<p class="level-item">Loading <b>${this.sourceUrl}</b>...</p>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="level">
|
||||
<div class="level-item has-text-centered">
|
||||
${this.renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
switch (this.state) {
|
||||
case "googledrive":
|
||||
return html`<wr-gdrive .sourceUrl=${this.sourceUrl} @load-ready=${this.onLoadReady}/>`;
|
||||
case "googledrive":
|
||||
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>
|
||||
<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``}
|
||||
case "started":
|
||||
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>`
|
||||
: ""}
|
||||
</div>`;
|
||||
|
||||
${!this.embed ? html`
|
||||
<button @click="${this.onCancel}" class="button is-danger">Cancel</button>` : ""}
|
||||
</div>`;
|
||||
|
||||
case "errored":
|
||||
return html`
|
||||
<div class="has-text-left">
|
||||
case "errored":
|
||||
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>`}
|
||||
</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>`}
|
||||
</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>
|
||||
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>
|
||||
<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"/>`;
|
||||
|
||||
case "waiting":
|
||||
default:
|
||||
return html`<progress
|
||||
class="progress is-primary is-large"
|
||||
style="max-width: 400px"
|
||||
/>`;
|
||||
}
|
||||
}
|
||||
|
||||
async onAskPermission() {
|
||||
const result = await this.fileHandle.requestPermission({mode: "read"});
|
||||
const result = await this.fileHandle.requestPermission({ mode: "read" });
|
||||
if (result === "granted") {
|
||||
this.doLoad();
|
||||
}
|
||||
|
||||
+102
-63
@@ -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,23 +49,23 @@ class FaIcon extends LitElement
|
||||
svg: { type: String },
|
||||
size: { type: String },
|
||||
width: { type: String },
|
||||
height: { type: String }
|
||||
height: { type: String },
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1.0em;
|
||||
}
|
||||
:host svg {
|
||||
fill: var(--fa-icon-fill-color, currentcolor);
|
||||
width: var(--fa-icon-width, 19px);
|
||||
height: var(--fa-icon-height, 19px);
|
||||
}
|
||||
:host {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
line-height: 1em;
|
||||
}
|
||||
:host svg {
|
||||
fill: var(--fa-icon-fill-color, currentcolor);
|
||||
width: var(--fa-icon-width, 19px);
|
||||
height: var(--fa-icon-height, 19px);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -103,37 +103,55 @@ class AnimLogo extends FaIcon
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
#wrlogo #stop5687 {
|
||||
animation: animLeft 7s linear infinite;
|
||||
}
|
||||
#wrlogo #stop5687 {
|
||||
animation: animLeft 7s linear infinite;
|
||||
}
|
||||
|
||||
#wrlogo #stop5689 {
|
||||
animation: animRight 7s linear infinite;
|
||||
}
|
||||
#wrlogo #stop5689 {
|
||||
animation: animRight 7s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes animLeft {
|
||||
0% {stop-color: #4876ff}
|
||||
25% {stop-color: #1b0921}
|
||||
50% {stop-color: #4876ff}
|
||||
75% {stop-color: #04cdff}
|
||||
100% {stop-color: #4876ff}
|
||||
}
|
||||
@keyframes animLeft {
|
||||
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}
|
||||
}
|
||||
@keyframes animRight {
|
||||
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,35 +163,41 @@ 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);
|
||||
}
|
||||
.modal-background {
|
||||
background-color: rgba(10, 10, 10, 0.5);
|
||||
}
|
||||
|
||||
.modal-card-head {
|
||||
background-color: var(--background, #97a1ff);
|
||||
}
|
||||
.modal-card-head {
|
||||
background-color: var(--background, #97a1ff);
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
max-width: var(--modal-width, 640px)
|
||||
}
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
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>
|
||||
@@ -187,10 +211,25 @@ class WrModal extends LitElement
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("fa-icon", FaIcon);
|
||||
customElements.define("fa-icon", FaIcon);
|
||||
customElements.define("wr-anim-logo", AnimLogo);
|
||||
|
||||
customElements.define("wr-modal", WrModal);
|
||||
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,
|
||||
};
|
||||
|
||||
+128
-79
@@ -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;
|
||||
@@ -143,27 +140,27 @@ class PageEntry extends LitElement
|
||||
|
||||
static sidebarStyles(prefix = css``) {
|
||||
return css`
|
||||
${prefix} .col-date {
|
||||
margin-left: calc(24px + 1rem);
|
||||
display: none;
|
||||
}
|
||||
${prefix} .col-date div {
|
||||
display: inline;
|
||||
}
|
||||
${prefix} .col-index {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
margin-top: -0.75em;
|
||||
}
|
||||
${prefix} .columns {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
${prefix} .is-inline-date {
|
||||
display: initial !important;
|
||||
font-style: italic;
|
||||
}
|
||||
${prefix} .col-date {
|
||||
margin-left: calc(24px + 1rem);
|
||||
display: none;
|
||||
}
|
||||
${prefix} .col-date div {
|
||||
display: inline;
|
||||
}
|
||||
${prefix} .col-index {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
margin-top: -0.75em;
|
||||
}
|
||||
${prefix} .columns {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
${prefix} .is-inline-date {
|
||||
display: initial !important;
|
||||
font-style: italic;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -180,72 +177,114 @@ 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">
|
||||
<label class="checkbox">
|
||||
<input @change=${this.onSendSelToggle} type="checkbox" .checked="${this.selected}">
|
||||
</label>
|
||||
</div>` : ""}
|
||||
${editable
|
||||
? html` <div class="check-select">
|
||||
<label class="checkbox">
|
||||
<input
|
||||
@change=${this.onSendSelToggle}
|
||||
type="checkbox"
|
||||
.checked="${this.selected}"
|
||||
/>
|
||||
</label>
|
||||
</div>`
|
||||
: ""}
|
||||
|
||||
<div class="columns">
|
||||
${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>
|
||||
</div>
|
||||
<div class="column">
|
||||
<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>`}
|
||||
</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)}">
|
||||
<p class="is-size-6 has-text-weight-bold has-text-link text">
|
||||
<keyword-mark keywords="${this.query}">${p.title || p.url}</keyword-mark>
|
||||
<div class="columns">
|
||||
${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>
|
||||
</div>
|
||||
<div class="column">
|
||||
<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>`}
|
||||
</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``}
|
||||
</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,
|
||||
)}"
|
||||
>
|
||||
<p class="is-size-6 has-text-weight-bold has-text-link text">
|
||||
<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-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``}
|
||||
</div>
|
||||
${hasSize
|
||||
? html` <div class="media-right" style="margin-right: 2em">
|
||||
${prettyBytes(p.size)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
${hasSize ? html`
|
||||
<div class="media-right" style="margin-right: 2em">
|
||||
${prettyBytes(p.size)}
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
|
||||
async updateFavIcon() {
|
||||
if (!this.page.favIconUrl) {
|
||||
if (!this.page.favIconUrl) {
|
||||
this.favIconData = null;
|
||||
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,18 +365,26 @@ 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() {
|
||||
const page = this.page;
|
||||
this.dispatchEvent(new CustomEvent("delete-page", {detail: {page}}));
|
||||
this.dispatchEvent(new CustomEvent("delete-page", { detail: { page } }));
|
||||
}
|
||||
|
||||
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 } }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+374
-211
@@ -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",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -92,7 +90,7 @@ class Pages extends LitElement
|
||||
selectedPages: { type: Set },
|
||||
allSelected: { type: Boolean },
|
||||
|
||||
menuActive: {type: Boolean },
|
||||
menuActive: { type: Boolean },
|
||||
|
||||
sortKey: { type: String },
|
||||
sortDesc: { type: Boolean },
|
||||
@@ -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 {
|
||||
@@ -193,19 +193,21 @@ class Pages extends LitElement
|
||||
|
||||
// normalize the date
|
||||
for (const page of this.filteredPages) {
|
||||
const {timestamp, date} = getPageDateTS(page);
|
||||
const { timestamp, date } = getPageDateTS(page);
|
||||
page.timestamp = timestamp;
|
||||
page.date = date;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.changeNeeded = false;
|
||||
const data = {query: this.query, currList: this.currList};
|
||||
const data = { query: this.query, currList: this.currList };
|
||||
this.sendChangeEvent(data);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -214,7 +216,7 @@ class Pages extends LitElement
|
||||
}
|
||||
|
||||
sendChangeEvent(data) {
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", { detail: { data } }));
|
||||
}
|
||||
|
||||
addPages(pages) {
|
||||
@@ -223,19 +225,24 @@ class Pages extends LitElement
|
||||
this.flex = flex;
|
||||
this.textPages = pages;
|
||||
|
||||
this.hasExtraPages = this.textPages && this.collInfo && this.collInfo.pages &&
|
||||
this.textPages.length > this.collInfo.pages.length;
|
||||
|
||||
return Promise.all(pages.map((page, index) => {
|
||||
let text = page.url;
|
||||
if (page.title) {
|
||||
text += " " + page.title;
|
||||
}
|
||||
if (page.text) {
|
||||
text += " " + page.text;
|
||||
}
|
||||
return flex.addAsync(index, text);
|
||||
}));
|
||||
this.hasExtraPages =
|
||||
this.textPages &&
|
||||
this.collInfo &&
|
||||
this.collInfo.pages &&
|
||||
this.textPages.length > this.collInfo.pages.length;
|
||||
|
||||
return Promise.all(
|
||||
pages.map((page, index) => {
|
||||
let text = page.url;
|
||||
if (page.title) {
|
||||
text += " " + page.title;
|
||||
}
|
||||
if (page.text) {
|
||||
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 {
|
||||
@@ -472,43 +479,44 @@ class Pages extends LitElement
|
||||
|
||||
static sidebarStyles(prefix = css``) {
|
||||
return css`
|
||||
${prefix} .main.columns {
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
${prefix} .main.columns {
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
${prefix} .index-bar-menu {
|
||||
max-height: 75px;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
${prefix} .index-bar-menu {
|
||||
max-height: 75px;
|
||||
overflow-y: auto;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
${prefix} .column.main-content {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
${prefix} .column.main-content {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
width: 100%;
|
||||
min-height: 0px;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
width: 100%;
|
||||
min-height: 0px;
|
||||
height: 100%;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
${prefix} .mobile-header {
|
||||
margin: 0.5rem;
|
||||
margin-left: 1.0rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-flow: row wrap;
|
||||
min-height: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
${prefix} .mobile-header {
|
||||
margin: 0.5rem;
|
||||
margin-left: 1rem;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-flow: row wrap;
|
||||
min-height: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
${prefix} .menu {
|
||||
font-size: 0.80rem;
|
||||
}`;
|
||||
${prefix} .menu {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
onSelectList(event) {
|
||||
@@ -525,68 +533,130 @@ class Pages extends LitElement
|
||||
const currList = this.currList;
|
||||
|
||||
return html`
|
||||
<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>` : ""}
|
||||
<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="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>`
|
||||
: ""}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="main columns">
|
||||
<div class="column index-bar is-one-fifth ${this.isSidebar ? "is-hidden-mobile" : ""}">
|
||||
<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">
|
||||
<label class="checkbox">
|
||||
<input
|
||||
@change=${(e) =>
|
||||
(this.showAllPages = e.currentTarget.checked)}
|
||||
type="checkbox"
|
||||
.checked="${this.showAllPages}"
|
||||
/>
|
||||
Show Non-Seed Pages
|
||||
</label>
|
||||
</span>`
|
||||
: ""}
|
||||
|
||||
${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>`}
|
||||
<span
|
||||
class="num-results is-hidden-mobile"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${this.formatResults()}</span
|
||||
>
|
||||
|
||||
${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}">
|
||||
Show Non-Seed Pages
|
||||
</label>
|
||||
</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.renderDownloadMenu()}
|
||||
</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>
|
||||
</li>
|
||||
${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>`)}
|
||||
</ul>
|
||||
${this.editable
|
||||
? html` <div class="index-bar-actions">
|
||||
${this.renderDownloadMenu()}
|
||||
</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
|
||||
>
|
||||
</li>
|
||||
${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>`,
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="column main-content">
|
||||
<div
|
||||
class="is-sr-only"
|
||||
role="heading"
|
||||
aria-level="${this.isSidebar ? "3" : "2"}"
|
||||
>
|
||||
Page List
|
||||
</div>
|
||||
${this.renderPages()}
|
||||
</div>
|
||||
` : ""}
|
||||
</div>
|
||||
<div class="column main-content">
|
||||
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "3": "2"}">Page List</div>
|
||||
${this.renderPages()}
|
||||
</div>
|
||||
</div>
|
||||
${this.renderDeleteModal()}
|
||||
${this.renderDeleteModal()}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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,33 +708,79 @@ class Pages extends LitElement
|
||||
|
||||
renderPageHeader() {
|
||||
return html`
|
||||
${!this.isSidebar && this.editable && this.filteredPages.length ? html`
|
||||
<div class="check-select">
|
||||
<label class="checkbox">
|
||||
<input @change=${this.onSelectAll} type="checkbox" .checked="${this.allSelected}">
|
||||
</label>
|
||||
</div>` : html``}
|
||||
${!this.isSidebar && this.editable && this.filteredPages.length
|
||||
? html` <div class="check-select">
|
||||
<label class="checkbox">
|
||||
<input
|
||||
@change=${this.onSelectAll}
|
||||
type="checkbox"
|
||||
.checked="${this.allSelected}"
|
||||
/>
|
||||
</label>
|
||||
</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>` : ""}
|
||||
<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
|
||||
>`
|
||||
: ""}
|
||||
|
||||
<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>
|
||||
<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"
|
||||
.sortKey="${this.sortKey}"
|
||||
.sortDesc="${this.sortDesc}"
|
||||
.sortKeys="${Pages.sortKeys}"
|
||||
.data="${this.filteredPages}"
|
||||
pageResults="100"
|
||||
@sort-changed="${this.onSortChanged}"
|
||||
class="${this.filteredPages.length ? "" : "is-hidden"}">
|
||||
</wr-sorter>
|
||||
</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"
|
||||
.sortKey="${this.sortKey}"
|
||||
.sortDesc="${this.sortDesc}"
|
||||
.sortKeys="${Pages.sortKeys}"
|
||||
.data="${this.filteredPages}"
|
||||
pageResults="100"
|
||||
@sort-changed="${this.onSortChanged}"
|
||||
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;
|
||||
@@ -697,31 +840,32 @@ class Pages extends LitElement
|
||||
//const name = this.currList === 0 ? "All Pages" : this.collInfo.lists[this.currList - 1].title;
|
||||
return html`
|
||||
<div class="page-header has-text-weight-bold">
|
||||
${this.renderPageHeader()}
|
||||
${this.renderPageHeader()}
|
||||
</div>
|
||||
<ul class="scroller" @scroll="${this.onScroll}">
|
||||
${this.sortedPages.length ? html`
|
||||
${this.sortedPages.map((p, i) => {
|
||||
const selected = this.selectedPages.has(p.id);
|
||||
${this.sortedPages.length
|
||||
? html` ${this.sortedPages.map((p, i) => {
|
||||
const selected = this.selectedPages.has(p.id);
|
||||
|
||||
return html`
|
||||
<li class="page-entry ${selected ? "selected" : ""}">
|
||||
<wr-page-entry
|
||||
.index="${this.query || this.isSidebar ? i + 1 : 0}"
|
||||
.editable="${this.editable}"
|
||||
.selected="${selected}"
|
||||
.isCurrent="${this.isCurrPage(p)}"
|
||||
.isSidebar="${this.isSidebar}"
|
||||
.page="${p}"
|
||||
pid="${p.id}"
|
||||
@sel-page="${this.onSelectToggle}"
|
||||
@delete-page="${this.onDeleteConfirm}"
|
||||
replayPrefix="${this.collInfo.replayPrefix}"
|
||||
query="${this.query}"
|
||||
class="${this.isSidebar ? "sidebar" : ""}"
|
||||
>
|
||||
</wr-page-entry>
|
||||
</li>`; })}` : html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`}
|
||||
return html` <li class="page-entry ${selected ? "selected" : ""}">
|
||||
<wr-page-entry
|
||||
.index="${this.query || this.isSidebar ? i + 1 : 0}"
|
||||
.editable="${this.editable}"
|
||||
.selected="${selected}"
|
||||
.isCurrent="${this.isCurrPage(p)}"
|
||||
.isSidebar="${this.isSidebar}"
|
||||
.page="${p}"
|
||||
pid="${p.id}"
|
||||
@sel-page="${this.onSelectToggle}"
|
||||
@delete-page="${this.onDeleteConfirm}"
|
||||
replayPrefix="${this.collInfo.replayPrefix}"
|
||||
query="${this.query}"
|
||||
class="${this.isSidebar ? "sidebar" : ""}"
|
||||
>
|
||||
</wr-page-entry>
|
||||
</li>`;
|
||||
})}`
|
||||
: html`<p class="mobile-header">${this.getNoResultsMessage()}</p>`}
|
||||
</ul>
|
||||
`;
|
||||
}
|
||||
@@ -741,13 +885,17 @@ class Pages extends LitElement
|
||||
|
||||
const title = input.value;
|
||||
|
||||
const body = JSON.stringify({title});
|
||||
const body = JSON.stringify({ title });
|
||||
|
||||
fetch(`${this.collInfo.apiPrefix}/metadata`, {method: "POST", body}).then((res) => {
|
||||
if (res.status === 200) {
|
||||
this.dispatchEvent(new CustomEvent("coll-update", {detail: {title}}));
|
||||
}
|
||||
});
|
||||
fetch(`${this.collInfo.apiPrefix}/metadata`, { method: "POST", body }).then(
|
||||
(res) => {
|
||||
if (res.status === 200) {
|
||||
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", () => {
|
||||
this.menuActive = false;
|
||||
}, {once: true});
|
||||
document.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
this.menuActive = false;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -780,13 +932,13 @@ class Pages extends LitElement
|
||||
}
|
||||
|
||||
onSelectToggle(event) {
|
||||
const {page, selected} = event.detail;
|
||||
const { page, selected } = event.detail;
|
||||
if (selected) {
|
||||
this.selectedPages.add(page);
|
||||
} 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);
|
||||
@@ -826,7 +981,7 @@ class Pages extends LitElement
|
||||
this.toDeletePages = this.selectedPages;
|
||||
this.toDeletePage = null;
|
||||
} else {
|
||||
// else, delete just the page
|
||||
// else, delete just the page
|
||||
this.toDeletePages = [page.id];
|
||||
this.toDeletePage = page;
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -862,7 +1019,7 @@ class Pages extends LitElement
|
||||
if (inx < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
this.collInfo.pages.splice(inx, 1);
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
+33
-22
@@ -1,13 +1,14 @@
|
||||
// ===========================================================================
|
||||
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 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
|
||||
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);
|
||||
}
|
||||
@@ -33,21 +41,19 @@ function getPageDateTS(page) {
|
||||
let date = null;
|
||||
try {
|
||||
date = new Date(page.ts || page.date);
|
||||
} catch (e) {
|
||||
} catch (e) {
|
||||
// leave date unchanged in case of error
|
||||
}
|
||||
|
||||
const timestamp = (date && !isNaN(date)) ? getTS(date.toISOString()) : "";
|
||||
return {date, timestamp};
|
||||
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 {
|
||||
@@ -69,10 +74,9 @@ async function sourceToId(url) {
|
||||
|
||||
const digest = await digestMessage(url, "SHA-256");
|
||||
const coll = "id-" + digest.slice(0, 12);
|
||||
return {url, coll};
|
||||
return { url, coll };
|
||||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
// simple parse of scheme, host, rest of path
|
||||
// not using URL due to different browser behavior
|
||||
@@ -102,8 +106,15 @@ function parseURLSchemeHostPath(url) {
|
||||
path = "";
|
||||
}
|
||||
|
||||
return {scheme, host, path};
|
||||
return { scheme, host, path };
|
||||
}
|
||||
|
||||
|
||||
export { digestMessage, tsToDate, getTS, getPageDateTS, getReplayLink, sourceToId, parseURLSchemeHostPath };
|
||||
export {
|
||||
digestMessage,
|
||||
tsToDate,
|
||||
getTS,
|
||||
getPageDateTS,
|
||||
getReplayLink,
|
||||
sourceToId,
|
||||
parseURLSchemeHostPath,
|
||||
};
|
||||
|
||||
+155
-75
@@ -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 = "";
|
||||
@@ -23,7 +20,7 @@ class Replay extends LitElement
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
collInfo: {type: Object },
|
||||
collInfo: { type: Object },
|
||||
sourceUrl: { type: String },
|
||||
|
||||
// external url set from parent
|
||||
@@ -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();
|
||||
@@ -61,7 +67,7 @@ class Replay extends LitElement
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.authFileHandle = null;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -97,30 +109,49 @@ class Replay extends LitElement
|
||||
if (this.iframeUrl && changedProperties.has("iframeUrl")) {
|
||||
this.waitForLoad();
|
||||
|
||||
const detail = {title: "Archived Page", replayTitle: false};
|
||||
this.dispatchEvent(new CustomEvent("update-title", {bubbles: true, composed: true, detail}));
|
||||
const detail = { title: "Archived Page", replayTitle: false };
|
||||
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;
|
||||
}
|
||||
@@ -282,49 +333,78 @@ class Replay extends LitElement
|
||||
}
|
||||
|
||||
render() {
|
||||
const title = `Replay of ${this.title ? `${this.title}:` :""} ${this.url}`;
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
${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>
|
||||
Authorization Needed
|
||||
</p>
|
||||
${!this.iframeUrl
|
||||
? html` <div class="panel intro-panel">
|
||||
<p class="panel-heading">Replay Web Page</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
|
||||
.sourceUrl="${this.sourceUrl}"
|
||||
.state="trymanual"
|
||||
.reauth="${true}"
|
||||
@load-ready="${this.onReAuthed}"/>`}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`: ""}
|
||||
</div>
|
||||
`}`;
|
||||
</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>
|
||||
|
||||
${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>
|
||||
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
|
||||
.sourceUrl="${this.sourceUrl}"
|
||||
.state="trymanual"
|
||||
.reauth="${true}"
|
||||
@load-ready="${this.onReAuthed}"
|
||||
/>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-15
@@ -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 = [];
|
||||
@@ -24,7 +22,7 @@ class Sorter extends LitElement
|
||||
return {
|
||||
id: { type: String },
|
||||
|
||||
pageResults: {type: Number},
|
||||
pageResults: { type: Number },
|
||||
|
||||
data: { type: Array },
|
||||
sortedData: { type: Array },
|
||||
@@ -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,9 +90,11 @@ 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}));
|
||||
this.dispatchEvent(new CustomEvent("sort-changed", { detail }));
|
||||
}
|
||||
|
||||
getMore(more = 100) {
|
||||
@@ -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}
|
||||
</option>
|
||||
`)}
|
||||
<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>`;
|
||||
}
|
||||
}
|
||||
|
||||
+176
-143
@@ -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,18 +39,19 @@ 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() {
|
||||
this.recalcSplitter(document.documentElement.clientWidth);
|
||||
|
||||
this.obs = new ResizeObserver((entries/*, observer*/) => {
|
||||
this.obs = new ResizeObserver((entries /*, observer*/) => {
|
||||
this.recalcSplitter(entries[0].contentRect.width);
|
||||
});
|
||||
|
||||
@@ -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,11 +100,11 @@ class Story extends LitElement
|
||||
|
||||
gutterSize: 4,
|
||||
|
||||
direction: this.splitDirection
|
||||
direction: this.splitDirection,
|
||||
};
|
||||
|
||||
this.splitter = Split([sidebar, content], opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async doLoadCurated() {
|
||||
@@ -134,7 +135,7 @@ class Story extends LitElement
|
||||
const title = page.title || page.url;
|
||||
const desc = curated.desc;
|
||||
|
||||
this.curatedPageMap[curated.list].push({url, ts, title, desc});
|
||||
this.curatedPageMap[curated.list].push({ url, ts, title, desc });
|
||||
}
|
||||
|
||||
this.scrollToList();
|
||||
@@ -142,161 +143,182 @@ class Story extends LitElement
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0px;
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0px;
|
||||
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host(.sidebar) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host(.sidebar) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host(.sidebar) .column.sidebar.is-one-fifth {
|
||||
width: 100% !important;
|
||||
}
|
||||
:host(.sidebar) .column.sidebar.is-one-fifth {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
${Story.sidebarStyles(unsafeCSS(":host(.sidebar)"))}
|
||||
${Story.sidebarStyles(unsafeCSS(":host(.sidebar)"))}
|
||||
|
||||
.desc p {
|
||||
margin-bottom: 1.0em;
|
||||
}
|
||||
.desc p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.columns {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-self: stretch;
|
||||
margin: 1.0em 0 0 0 !important;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.column.main-content {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
margin-left: 0.75em;
|
||||
}
|
||||
|
||||
.column.main-content.main-scroll {
|
||||
padding-right: 0.75em;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
ul.menu-list a.is-active {
|
||||
background-color: #55be6f;
|
||||
}
|
||||
ol {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.columns {
|
||||
margin-top: 0.75em;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-self: stretch;
|
||||
margin: 1em 0 0 0 !important;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.column.sidebar {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
.column.main-content {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0px;
|
||||
margin-left: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
${Story.sidebarStyles()}
|
||||
}
|
||||
.column.main-content.main-scroll {
|
||||
padding-right: 0.75em;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
ul.menu-list a.is-active {
|
||||
background-color: #55be6f;
|
||||
}
|
||||
ol {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.gutter.gutter-horizontal:hover {
|
||||
cursor: col-resize;
|
||||
}
|
||||
@media screen and (min-width: 769px) {
|
||||
.columns {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
.column.sidebar {
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
${Story.sidebarStyles()}
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.gutter.gutter-horizontal:hover {
|
||||
cursor: col-resize;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
static sidebarStyles(prefix = css``) {
|
||||
return css`
|
||||
${prefix} .columns {
|
||||
position: relative;
|
||||
}
|
||||
${prefix} .columns {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
${prefix} .column.sidebar {
|
||||
overflow-y: auto;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
${prefix} .column.sidebar {
|
||||
overflow-y: auto;
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
${prefix} .column.main-content {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
${prefix} .column.main-content {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
border-top: 1px solid black;
|
||||
border-top: 1px solid black;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
${prefix} .menu {
|
||||
font-size: 0.80rem;
|
||||
}`;
|
||||
${prefix} .menu {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const currListNum = this.currList;
|
||||
|
||||
return html`
|
||||
<div class="is-sr-only" role="heading" aria-level="${this.isSidebar ? "2": "1"}">
|
||||
Story for ${this.collInfo.title}
|
||||
</div>
|
||||
<div class="columns">
|
||||
<div class="column sidebar is-one-fifth">
|
||||
<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}"
|
||||
data-list="${list.id}"
|
||||
class="${currListNum === list.id ? "is-active" : ""}">${list.title}</a>
|
||||
</li>`)}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div
|
||||
class="is-sr-only"
|
||||
role="heading"
|
||||
aria-level="${this.isSidebar ? "2" : "1"}"
|
||||
>
|
||||
Story for ${this.collInfo.title}
|
||||
</div>
|
||||
<div @scroll=${this.onScroll} class="column main-content main-scroll">
|
||||
<section id="list-0" class="desc">
|
||||
<h2 class="has-text-centered title is-3">${this.collInfo.title}</h2>
|
||||
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ""}
|
||||
</section>
|
||||
${this.renderLists()}
|
||||
<div class="columns">
|
||||
<div class="column sidebar is-one-fifth">
|
||||
<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}"
|
||||
data-list="${list.id}"
|
||||
class="${currListNum === list.id ? "is-active" : ""}"
|
||||
>${list.title}</a
|
||||
>
|
||||
</li>`,
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</div>
|
||||
<div @scroll=${this.onScroll} class="column main-content main-scroll">
|
||||
<section id="list-0" class="desc">
|
||||
<h2 class="has-text-centered title is-3">${this.collInfo.title}</h2>
|
||||
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ""}
|
||||
</section>
|
||||
${this.renderLists()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
renderLists() {
|
||||
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``}
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
`)}`;
|
||||
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``}
|
||||
</ol>
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)}`;
|
||||
}
|
||||
|
||||
renderCPage(p) {
|
||||
@@ -304,18 +326,21 @@ 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>
|
||||
<p>${date.toLocaleString()}</p>
|
||||
<p>${p.desc}</p>
|
||||
</div>
|
||||
<hr/>
|
||||
<hr />
|
||||
</li>`;
|
||||
}
|
||||
|
||||
@@ -330,7 +355,7 @@ class Story extends LitElement
|
||||
}
|
||||
|
||||
sendChangeEvent(data) {
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", { detail: { data } }));
|
||||
}
|
||||
|
||||
onClickScroll(event) {
|
||||
@@ -347,7 +372,7 @@ class Story extends LitElement
|
||||
this.currList = 0;
|
||||
}
|
||||
|
||||
const opts = {behavior: "smooth", block: "nearest", inline: "nearest"};
|
||||
const opts = { behavior: "smooth", block: "nearest", inline: "nearest" };
|
||||
this.clickTime = new Date().getTime();
|
||||
const curr = this.renderRoot.getElementById("list-" + this.currList);
|
||||
if (curr) {
|
||||
@@ -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,13 +414,15 @@ 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"};
|
||||
const opts = { behavior: "smooth", block: "nearest", inline: "nearest" };
|
||||
sel.scrollIntoView(opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, { type: "text/html", content: INDEX_HTML });
|
||||
staticData.set(prefix + "index.html", {
|
||||
type: "text/html",
|
||||
content: INDEX_HTML,
|
||||
});
|
||||
|
||||
const sp = new URLSearchParams(self.location.search);
|
||||
|
||||
@@ -21,10 +23,7 @@ if (self.registration) {
|
||||
defaultConfig.injectScripts = ["ruffle/ruffle.js"];
|
||||
}
|
||||
|
||||
self.sw = new SWReplay({staticData, defaultConfig});
|
||||
self.sw = new SWReplay({ staticData, defaultConfig });
|
||||
} else {
|
||||
new WorkerLoader(self);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
+29
-19
@@ -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;
|
||||
@@ -20,7 +23,7 @@ export class SWManager
|
||||
|
||||
register() {
|
||||
let resolve, reject;
|
||||
|
||||
|
||||
const p = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
@@ -37,20 +40,24 @@ 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);
|
||||
};
|
||||
|
||||
|
||||
register(this.scope + this.name, {
|
||||
registrationOptions: { scope: this.scope },
|
||||
registered () {
|
||||
registered() {
|
||||
console.log("Service worker is registered");
|
||||
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) {
|
||||
@@ -120,13 +125,18 @@ 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>
|
||||
</div>
|
||||
<div style="white-space: pre-wrap; text-align: center" >${msg}</div>
|
||||
</section>
|
||||
<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>
|
||||
</div>
|
||||
<div style="white-space: pre-wrap; text-align: center">${msg}</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+238
-153
@@ -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: "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: "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: "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: "" },
|
||||
];
|
||||
}
|
||||
|
||||
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") ||
|
||||
changedProperties.has("urlSearchType") ||
|
||||
changedProperties.has("currMime")) {
|
||||
|
||||
if (
|
||||
changedProperties.has("query") ||
|
||||
changedProperties.has("urlSearchType") ||
|
||||
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);
|
||||
}
|
||||
@@ -205,121 +218,137 @@ class URLResources extends LitElement
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(css`
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
min-width: 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host(.sidebar) .is-hidden-tablet {
|
||||
display: flex !important;
|
||||
}
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
min-width: 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host(.sidebar) .is-hidden-tablet {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
:host(.sidebar) .is-hidden-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
:host(.sidebar) .is-hidden-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:host(.sidebar) .level, :host(.sidebar) .level-left, :host(.sidebar) .level-right {
|
||||
display: block !important;
|
||||
}
|
||||
:host(.sidebar) .level,
|
||||
:host(.sidebar) .level-left,
|
||||
:host(.sidebar) .level-right {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
:host(.sidebar) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
:host(.sidebar) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host(.sidebar) .column {
|
||||
width: 100% !important;
|
||||
}
|
||||
:host(.sidebar) .column {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.notification {
|
||||
width: 100%;
|
||||
}
|
||||
.all-results {
|
||||
margin: 0 0 0 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.main-scroll {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.columns {
|
||||
margin: 0px;
|
||||
}
|
||||
thead {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
table th:not([align]) {
|
||||
text-align: left;
|
||||
}
|
||||
.result {
|
||||
border-bottom: 1px #dbdbdb solid;
|
||||
min-height: fit-content;
|
||||
}
|
||||
.results-head {
|
||||
border-bottom: 2px #dbdbdb solid;
|
||||
margin-right: 16px;
|
||||
min-height: fit-content;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.results-head a {
|
||||
color: black;
|
||||
}
|
||||
.all-results .column {
|
||||
word-break: break-word;
|
||||
}
|
||||
div.sort-header {
|
||||
padding: 10px;
|
||||
margin-bottom: 0px !important;
|
||||
min-height: fit-content;
|
||||
}
|
||||
.flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
.asc:after {
|
||||
content: "▼";
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.desc:after {
|
||||
content: "▲";
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.num-results {
|
||||
margin-left: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
.notification {
|
||||
width: 100%;
|
||||
}
|
||||
.all-results {
|
||||
margin: 0 0 0 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.main-scroll {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.columns {
|
||||
margin: 0px;
|
||||
}
|
||||
thead {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
table th:not([align]) {
|
||||
text-align: left;
|
||||
}
|
||||
.result {
|
||||
border-bottom: 1px #dbdbdb solid;
|
||||
min-height: fit-content;
|
||||
}
|
||||
.results-head {
|
||||
border-bottom: 2px #dbdbdb solid;
|
||||
margin-right: 16px;
|
||||
min-height: fit-content;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.results-head a {
|
||||
color: black;
|
||||
}
|
||||
.all-results .column {
|
||||
word-break: break-word;
|
||||
}
|
||||
div.sort-header {
|
||||
padding: 10px;
|
||||
margin-bottom: 0px !important;
|
||||
min-height: fit-content;
|
||||
}
|
||||
.flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
.asc:after {
|
||||
content: "▼";
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.desc:after {
|
||||
content: "▲";
|
||||
font-size: 0.75em;
|
||||
}
|
||||
.num-results {
|
||||
margin-left: 1em;
|
||||
font-style: italic;
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
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: </span>
|
||||
<div class="select">
|
||||
<select @change="${this.onChangeTypeSearch}">
|
||||
${URLResources.filters.map((filter) => html`
|
||||
<option value="${filter.filter}"
|
||||
?selected="${filter.filter === this.currMime}">
|
||||
${filter.name}
|
||||
</option>
|
||||
`)}
|
||||
${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}"> Contains</label>
|
||||
<label class="radio has-text-left"><input type="radio" name="urltype" value="prefix" ?checked="${this.urlSearchType === "prefix"}" @click="${this.onClickUrlType}"> Prefix</label>
|
||||
<label class="radio has-text-left"><input type="radio" name="urltype" value="exact" ?checked="${this.urlSearchType === "exact"}" @click="${this.onClickUrlType}"> 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}"> Contains</label>
|
||||
<label class="radio has-text-left"><input type="radio" name="urltype" value="prefix" ?checked="${
|
||||
this.urlSearchType === "prefix"
|
||||
}" @click="${this.onClickUrlType}"> Prefix</label>
|
||||
<label class="radio has-text-left"><input type="radio" name="urltype" value="exact" ?checked="${
|
||||
this.urlSearchType === "exact"
|
||||
}" @click="${this.onClickUrlType}"> 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`
|
||||
<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>
|
||||
</tr>
|
||||
`) : html`<tr class="section"><td colspan="4"><i>No Results Found.</i></td></tr>`}
|
||||
${
|
||||
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>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
: html`<tr class="section">
|
||||
<td colspan="4"><i>No Results Found.</i></td>
|
||||
</tr>`
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
@@ -399,7 +484,7 @@ class URLResources extends LitElement
|
||||
ts: event.currentTarget.getAttribute("data-ts"),
|
||||
};
|
||||
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
|
||||
this.dispatchEvent(new CustomEvent("coll-tab-nav", { detail: { data } }));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user