big ui update, page search + full text

- split file chooser from coll index, insert as header, make file chooser collapsible
- revamp collection index to be responsive
- add separate sorter component, used for collection index and pages
- combine 'curated lists' + pages, with page search + narrative
- ensure page + narrative lists are collapsible
- page search: support title, url and full text search if available, using flexsearch
- page search: keyword highlighting, load all pages, showing in increments of 100
- page and resource search: delay search to 1/4 sec of inactivity to avoid too many queries
- electron app: ensure noCache is set to true for local files
- embedding: remove separate embed.js, just use ui.js for embed as well
- embedding: support embedding iframe in a local origin with just sw.js and ui.js: sw.js serves index.html,
<replay-web-page> embed injects ui.js into iframe
- sw.js, new service worker entry point
- sw and ww inited only from <replay-app-main>, ui.js loaded from <replay-web-page> embed component
This commit is contained in:
Ilya Kreymer
2020-05-26 14:43:28 -07:00
parent ff6de51132
commit 03023523b7
21 changed files with 1743 additions and 1069 deletions
-208
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -9,6 +9,8 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",
"bulma": "^0.8.2",
"flexsearch": "^0.6.32",
"keyword-mark-element": "^0.1.2",
"lit-element": "^2.3.1",
"marked": "^1.0.0",
"mime-types": "^2.1.27",
+25 -3
View File
@@ -1,6 +1,8 @@
import { LitElement, html, css } from 'lit-element';
import { wrapCss, rwpLogo, IS_APP } from './misc';
import { registerSW } from './pageutils';
import prettyBytes from 'pretty-bytes';
import fasHelp from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
@@ -19,10 +21,15 @@ class App extends LitElement
this.collInfo = null;
this.showTerms = false;
this.pageParams = {};
this.inited = false;
registerSW(__SW_NAME__);
}
static get properties() {
return {
inited: { type: Boolean },
pageParams: { type: Object },
sourceUrl: { type: String },
navMenuShown: { type: Boolean },
@@ -63,6 +70,9 @@ class App extends LitElement
div.navbar-menu fa-icon {
vertical-align: sub;
}
.tagline {
margin-top: 1.0rem;
}
@media screen and (min-width: 840px) {
.menu-only {
@@ -82,8 +92,8 @@ class App extends LitElement
padding-left: 0px;
}
.logo-text:hover {
background-color: unset;
a.navbar-item.logo-text:hover {
background-color: initial;
}
}
@@ -97,6 +107,9 @@ class App extends LitElement
}
render() {
if (!this.inited) {
return html``;
}
return html`
${!this.embed ? html`
<nav class="navbar has-background-info" role="navigation" aria-label="main navigation">
@@ -162,14 +175,22 @@ class App extends LitElement
` : ''}
${this.sourceUrl ? html`
<wr-coll .loadInfo="${this.loadInfo}"
sourceUrl="${this.sourceUrl}"
embed="${this.embed}"
@replay-favicons=${this.onFavIcons}
@coll-loaded=${this.onCollLoaded}></wr-coll>
` : html`
<wr-coll-index @load-start=${this.onStartLoad}></wr-coll-index>
<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.</p>
` : ``}
<wr-chooser slot="header" @load-start=${this.onStartLoad}></wr-chooser>
</wr-coll-index>
`}
${this.showTerms ? html`
<div class="modal is-active">
<div class="modal-background" @click="${(e) => this.showTerms = false}"></div>
@@ -241,6 +262,7 @@ class App extends LitElement
}
initRoute() {
this.inited = true;
this.pageParams = new URLSearchParams(window.location.search);
// Google Drive
+138
View File
@@ -0,0 +1,138 @@
import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc';
import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg';
// ===========================================================================
class Chooser extends LitElement
{
constructor() {
super();
this.fileDisplayName = "";
this.file = null;
}
onChooseFile(event) {
if (event.currentTarget.files.length === 0) {
return;
}
this.file = event.currentTarget.files[0];
// file.path only available in electron app
this.fileDisplayName = "file://" + (this.file.path || this.file.name);
this.requestUpdate();
}
onStartLoad(event) {
event.preventDefault();
const loadInfo = {sourceUrl: this.fileDisplayName};
if (this.file) {
loadInfo.isFile = true;
// file.path only available in electron app
loadInfo.loadUrl = this.file.path ? __APP_FILE_SERVE_PREFIX__ + this.file.path : URL.createObjectURL(this.file);
loadInfo.noCache = this.file.path !== undefined;
loadInfo.name = this.fileDisplayName;
}
this.dispatchEvent(new CustomEvent("load-start", {detail: loadInfo}));
return false;
}
onInput(event) {
this.fileDisplayName = event.currentTarget.value;
if (this.file && this.fileDisplayName && this.fileDisplayName.startsWith("file://")) {
this.file = null;
this.fileDisplayName = "";
}
}
static get styles() {
return wrapCss(css`
.extra-padding {
padding: 2em;
}
.less-padding {
padding-top: 1.0em;
padding-bottom: 1.0em;
}
div.field.has-addons {
flex: auto;
}
form {
flex-grow: 1;
}
input.input.file-name:invalid {
border: 1px dashed red;
}
input.input.file-name {
max-width: 100%;
border-width: 1px;
margin-left: -1px;
}
@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;
}
}
`);
};
render() {
return html`
<section class="section less-padding">
<nav class="panel is-warning">
<p class="panel-heading">Load Web Archive</p>
<div class="extra-padding panel-block file has-name">
<form class="content is-flex" @submit="${this.onStartLoad}">
<label class="file-label">
<input class="file-input"
@click="${(e) => e.currentTarget.value = null}"
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">
<span class="file-cta">
<span class="file-icon">
<fa-icon size="0.9em" .svg=${fasUpload}></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|s3):\/\/.*\.(warc|warc.gz|har|zip|wacz))|(googledrive:\/\/.+)"
.value="${this.fileDisplayName}"
@input="${this.onInput}"
autocomplete="off"
placeholder="Enter a URL or click 'Choose File' to select a (WARC, HAR, WBN, or WACZ) archive source">
</p>
<div class="control">
<button type="submit" class="button is-hidden-mobile is-primary">Load</button>
</div>
</div>
</form>
</div>
</nav>
</section>`;
}
}
customElements.define("wr-chooser", Chooser);
export { Chooser };
+119 -189
View File
@@ -1,38 +1,54 @@
import { LitElement, html, css } from 'lit-element';
import { wrapCss, IS_APP } from './misc';
import { wrapCss } from './misc';
import prettyBytes from 'pretty-bytes';
import fasUpload from '@fortawesome/fontawesome-free/svgs/solid/upload.svg';
import fasCopy from '@fortawesome/fontawesome-free/svgs/regular/copy.svg';
import fasArrowUp from '@fortawesome/fontawesome-free/svgs/solid/angle-double-up.svg';
import fasArrowDown from '@fortawesome/fontawesome-free/svgs/solid/angle-double-down.svg';
// ===========================================================================
class CollIndex extends LitElement
{
constructor() {
super();
this.colls = [];
this.sortedColls = [];
this.sortKey = "title";
this.sortDesc = false;
//this.sortKey = localStorage.getItem("index:sortKey") || "title";
//this.sortDesc = localStorage.getItem("index:sortDesc") === "1";
this.fileDisplayName = "";
this.file = null;
this.hideHeader = localStorage.getItem("index:hideHeader") === "1";
this._deleting = {};
}
static get sortKeys() {
return [
{key: "title",
name: "Title"},
{key: "sourceUrl",
name: "Source"},
{key: "ctime",
name: "Date Created"},
{key: "size",
name: "Total Size"}
];
}
static get properties() {
return {
colls: { type: Array },
sortedColls: { type: Array },
sortKey: { type: String },
sortDesc: { type: Boolean },
fileDisplayName: { type: String },
hideHeader: { type: Boolean },
_deleting: { type: Object }
}
}
@@ -42,35 +58,19 @@ class CollIndex extends LitElement
}
updated(changedProperties) {
if (changedProperties.has("colls") ||
changedProperties.has("sortKey") ||
changedProperties.has("sortDesc")) {
this.sortColls();
if (changedProperties.has("hideHeader")) {
localStorage.setItem("index:hideHeader", this.hideHeader ? "1" : "0");
}
}
sortColls() {
this.sortedColls = [];
this.colls.forEach((coll) => {
coll.title = coll.title || coll.filename;
this.sortedColls.push(coll);
});
this.sortedColls.sort((first, second) => {
if (first[this.sortKey] === second[this.sortKey]) {
return 0;
}
return (this.sortDesc == (first[this.sortKey] < second[this.sortKey])) ? 1 : -1;
});
}
async loadColls() {
const resp = await fetch("./wabac/api/index");
try {
const json = await resp.json();
this.colls = json.colls;
this.colls = json.colls.map((coll) => {
coll.title = coll.title || coll.filename;
return coll;
});
} catch (e) {
// likely no sw registered yet
}
@@ -82,7 +82,12 @@ class CollIndex extends LitElement
event.preventDefault();
event.stopPropagation();
if (!this.sortedColls) {
return;
}
const index = Number(event.currentTarget.getAttribute("data-coll-index"));
const coll = this.sortedColls[index];
if (!coll || this._deleting[coll.sourceUrl]) {
@@ -100,41 +105,6 @@ class CollIndex extends LitElement
return false;
}
onChooseFile(event) {
if (event.currentTarget.files.length === 0) {
return;
}
this.file = event.currentTarget.files[0];
this.fileDisplayName = "file://" + (this.file.path || this.file.name);
this.requestUpdate();
}
onStartLoad(event) {
event.preventDefault();
const detail = {sourceUrl: this.fileDisplayName};
if (this.file) {
detail.isFile = true;
detail.loadUrl = this.file.path ? __APP_FILE_SERVE_PREFIX__ + this.file.path : URL.createObjectURL(this.file);
detail.name = this.fileDisplayName;
}
this.dispatchEvent(new CustomEvent("load-start", {detail}));
return false;
}
onInput(event) {
this.fileDisplayName = event.currentTarget.value;
if (this.file && this.fileDisplayName && this.fileDisplayName.startsWith("file://")) {
this.file = null;
this.fileDisplayName = "";
}
}
static get styles() {
return wrapCss(css`
:host {
@@ -149,12 +119,6 @@ class CollIndex extends LitElement
.no-top-padding {
padding-top: 1.0em;
}
div.field.has-addons {
flex: auto;
}
form, .level {
flex-grow: 1;
}
button.is-loading {
line-height: 1.5em;
height: 1.5em;
@@ -165,33 +129,10 @@ class CollIndex extends LitElement
nav.panel.is-light {
margin-bottom: 2em;
}
input.input.file-name:invalid {
border: 1px dashed red;
}
input.input.file-name {
max-width: 100%;
border-width: 1px;
margin-left: -1px;
}
table tr {
cursor: pointer;
}
tbody tr:hover {
background-color: aliceblue;
}
fa-icon {
vertical-align: middle;
}
.asc:after {
content: "▼";
font-size: 0.75em;
}
.desc:after {
content: "▲";
font-size: 0.75em;
}
.copy {
color: black;
margin: 0px;
@@ -205,112 +146,110 @@ class CollIndex extends LitElement
.copy:active {
background-color: lightgray;
}
.col-source {
.sort-header{
padding-bottom: 0.3rem;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.columns {
width: 100vw;
}
.column {
word-break: break-word;
position: relative;
}
.col-source:hover > .copy, .source-text:hover + .copy, .copy:hover {
.col-title:hover {
}
.col-title a {
display: block;
height: 100%;
}
.column:hover > .copy, .source-text:hover + .copy, .copy:hover {
display: inline;
}
.coll-block {
position: relative;
}
.delete {
position: absolute;
top: 10px;
right: 10px;
}
.minihead {
font-size: 10px;
font-weight: bold;
}
#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;
}
`);
}
render() {
return html`
<header class="${this.hideHeader ? 'closed' : ''}">
<slot name="header"></slot>
</header>
<section class="section no-top-padding">
${!IS_APP ? html`
<p class="subtitle has-text-centered">Explore and Replay Interactive Archived Webpages Directly in your Browser.</p>
` : ``}
<nav class="panel is-warning">
<p class="panel-heading">Load Web Archive</p>
<div class="extra-padding panel-block file has-name">
<form class="content is-flex" @submit="${this.onStartLoad}">
<label class="file-label">
<input class="file-input"
@click="${(e) => e.currentTarget.value = null}"
@change=${this.onChooseFile} type="file" id="fileupload" name="fileupload">
<span class="file-cta">
<span class="file-icon">
<fa-icon size="0.9em" .svg=${fasUpload}></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|s3):\/\/.*\.(warc|warc.gz|har|zip|wacz))|(googledrive:\/\/.+)"
.value="${this.fileDisplayName}"
@input="${this.onInput}"
autocomplete="off"
placeholder="Enter a URL or click 'Choose File' to select a (WARC, HAR, WBN, or WACZ) archive source">
</p>
<div class="control">
<button type="submit" class="button is-hidden-mobile is-primary">Load</button>
<div class="sort-header is-small">
<a @click=${(e) => this.hideHeader = !this.hideHeader} class="button is-small">
<span class="icon"><fa-icon .svg=${this.hideHeader ? fasArrowDown : fasArrowUp}></span>
<span>${this.hideHeader ? 'Expand' : 'Collapse'}</span>
</a>
<wr-sorter id="index"
.sortKeys="${CollIndex.sortKeys}"
.data="${this.colls}"
@sort-changed="${(e) => this.sortedColls = e.detail.sortedData}">
</wr-sorter>
</div>
</div>
</form>
</div>
</nav>
</section>
<section class="section no-top-padding">
<nav class="panel is-light">
<p class="panel-heading">Loaded Archives</p>
<div class="panel-heading"><span>Loaded Archives</span>
</div>
<div class="coll-list">
${this.sortedColls.length ? html`
<div class="panel-block">
<table class="table is-fullwidth">
<thead>
<tr>
<th @click="${this.onSort}" data-key="title"
class="${this.sortKey === "title" ? (this.sortDesc ? "desc" : "asc") : ''} col-title">
Title</th>
<th @click="${this.onSort}" data-key="sourceUrl"
class="${this.sortKey === "sourceUrl" ? (this.sortDesc ? "desc" : "asc") : ''} col-source">
Source URL (or Filename)</th>
<th @click="${this.onSort}" data-key="ctime"
class="${this.sortKey === "ctime" ? (this.sortDesc ? "desc" : "asc") : ''} col-ctime">
Date Loaded</th>
<th @click="${this.onSort}" data-key="size"
class="${this.sortKey === "size" ? (this.sortDesc ? "desc" : "asc") : ''} col-size">
Total Size</th>
<th></th>
</tr>
</thead>
<tbody>
${this.sortedColls.map((coll, i) => html`
<tr @click=${(e) => window.location.href = "?source=" + coll.sourceUrl}>
<td class="col-title"><span class="subtitle has-text-weight-bold">
<div class="coll-block panel-block">
<div class="columns">
<div class="column col-title is-4">
<span class="subtitle has-text-weight-bold">
<a href="?source=${coll.sourceUrl}">${coll.title || coll.filename}</a>
</td>
<td class="col-source"><span class="source-text">${coll.sourceUrl}&nbsp;</span>
</span>
</div>
<div class="column is-4">
<span class="source-text"><p class="minihead">Source</p>${coll.sourceUrl}&nbsp;</span>
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy"><fa-icon .svg="${fasCopy}"/></a>
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://") ? html`
<p><i>(${coll.filename})</i></p>` : ''}
</td>
<td class="col-date">${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</td>
<td class="col-size">${prettyBytes(Number(coll.size || 0))}</td>
<td class="col-delete">
</div>
<div class="column is-2"><p class="minihead">Date Loaded</p>${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}</div>
<div class="column is-2"><p class="minihead">Total Size</p>${prettyBytes(Number(coll.size || 0))}
</div>
</div>
${!this._deleting[coll.sourceUrl] ? html`
<button data-coll-index="${i}" @click="${this.onDeleteColl}" class="delete"></button>
<button class="delete" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
` : html`
<button class="button is-loading is-static"></button>`}
</td>
</tr>
<button class="button delete is-loading is-static"></button>`}
</div>
`)}
</tbody>
</table>
</div>` : html`
</div>
` : html`
<div class="panel-block extra-padding">
<i>No Archives so far! Archives loaded in the section above will appear here.</i>
</div>
@@ -327,17 +266,8 @@ class CollIndex extends LitElement
navigator.clipboard.writeText(sourceUrl);
return false;
}
}
onSort(event) {
const key = event.currentTarget.getAttribute("data-key");
if (key === this.sortKey) {
this.sortDesc = !this.sortDesc;
} else {
this.sortDesc = false;
this.sortKey = key;
}
}
}
customElements.define("wr-coll-index", CollIndex);
+48 -49
View File
@@ -3,9 +3,8 @@ import { wrapCss, IS_APP } from './misc';
import { sourceToId } from './pageutils';
import farListAlt from '@fortawesome/fontawesome-free/svgs/solid/list-alt.svg';
import fasPage from '@fortawesome/fontawesome-free/svgs/solid/file-alt.svg';
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
import farListAlt from '@fortawesome/fontawesome-free/svgs/regular/list-alt.svg';
import fasResources from '@fortawesome/fontawesome-free/svgs/regular/file-code.svg';
import farPlayCircle from '@fortawesome/fontawesome-free/svgs/regular/play-circle.svg';
@@ -15,6 +14,7 @@ class Coll extends LitElement
constructor() {
super();
this.sourceUrl = null;
this.inited = false;
this.baseApiPrefix = "./wabac/api";
this.baseReplayPrefix = "./wabac";
@@ -25,27 +25,27 @@ class Coll extends LitElement
this.collInfo = null;
this.hasCurated = false;
this._replaceLoc = false;
this._locUpdateNeeded = false;
this._locationHash = "";
this.tabData = {};
this.tabNames = ["pages", "resources", "replay"];
}
static get properties() {
return {
sourceUrl: { type: String },
loadInfo: { type: Object },
inited: { type: Boolean },
collInfo: { type: Object },
sourceUrl: { type: String },
loadInfo: { type: Object, attribute: false },
collInfo: { type: Object, attribute: false },
coll: { type: String },
hasCurated: { type: Boolean },
tabData: { type: Object },
tabData: { type: Object, attribute: false },
replayUrl: { type: String },
replayTS: { type: String },
@@ -56,6 +56,7 @@ class Coll extends LitElement
firstUpdated() {
//this.doUpdateInfo();
this.inited = true;
window.addEventListener("hashchange", (event) => this.onHashChange(event));
}
@@ -109,7 +110,6 @@ class Coll extends LitElement
if (resp.status != 200) {
this.collInfo = {};
this.hasCurated = false;
return;
}
@@ -121,21 +121,12 @@ class Coll extends LitElement
this.collInfo.title = this.collInfo.filename;
}
this.hasCurated = (this.collInfo.lists && this.collInfo.lists.length);
this.dispatchEvent(new CustomEvent("coll-loaded", {detail: {
collInfo: this.collInfo,
alreadyLoaded: true
}}));
const hash = window.location.hash;
if (hash) {
this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries());
}
if (this.collInfo.coll && !this.tabData.view) {
this.tabData = {...this.tabData, view: this.hasCurated ? "curated" : "resources"};
//this.replaceLoc = true;
}
this.onHashChange();
}
onCollLoaded(event) {
@@ -153,6 +144,13 @@ class Coll extends LitElement
this.tabData = Object.fromEntries(new URLSearchParams(hash.slice(1)).entries());
this._locationHash = hash;
}
if (this.collInfo.coll && !this.tabNames.includes(this.tabData.view)) {
this.tabData = {
...this.tabData,
view: this.collInfo.numPages || this.collInfo.numLists ? "pages" : "resources"
};
}
}
onTabClick(event) {
@@ -196,11 +194,11 @@ class Coll extends LitElement
}
}
@media screen and (min-width: 320px) {
.main.tabs {
position: absolute;
top: 0px;
left: 50%;
min-height: 41px;
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
z-index: 35;
@@ -212,10 +210,16 @@ class Coll extends LitElement
line-height: 1.5;
padding-top: 0.5em;
}
@media screen and (max-width: 319px) {
.main.tabs li a {
padding-right: 4px;
padding-left: 4px;
}
.main.tabs {
min-height: 41px;
.tabs span.icon {
margin: 0px !important;
}
}
#contents {
@@ -234,6 +238,8 @@ class Coll extends LitElement
}
render() {
if (!this.inited) return html``;
if (this.collInfo && !this.collInfo.coll) {
return html`
<wr-loader .loadInfo="${this.loadInfo}"
@@ -244,26 +250,17 @@ class Coll extends LitElement
${!this.embed ? html`
<div class="main tabs has-background-info is-centered">
<ul>
${this.hasCurated ? html`
<li class="${this.tabData.view === 'curated' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#curated" class="is-size-6">
<span class="icon"><fa-icon .svg="${farListAlt}"></fa-icon></span>
<span class="tab-label" title="Curated Lists">Curated Lists</span>
</a>
</li>
` : ``}
<li class="${this.tabData.view === 'pages' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#pages" class="is-size-6">
<span class="icon"><fa-icon .svg="${fasPage}"></fa-icon></span>
<span class="icon"><fa-icon .svg="${farListAlt}"></fa-icon></span>
<span class="tab-label" title="Pages">Pages</span>
</a>
</li>
<li class="${this.tabData.view === 'resources' ? 'is-active' : ''}">
<a @click="${this.onTabClick}" href="#resources" class="is-size-6">
<span class="icon"><fa-icon .svg="${fasSearch}"></fa-icon></span>
<span class="tab-label" title="URL Resources">URL Resources</span>
<span class="icon"><fa-icon .svg="${fasResources}"></fa-icon></span>
<span class="tab-label" title="URL Resources">Page Resources</span>
</a>
</li>
@@ -283,34 +280,36 @@ class Coll extends LitElement
}
renderCollTabs() {
const isPages = this.tabData.view === 'pages';
const isResources = this.tabData.view === 'resources';
const isReplay = this.tabData.view === 'replay';
return html`
<wr-coll-curated .collInfo="${this.collInfo}"
currList="${this.tabData.currList || 0}"
@coll-tab-nav="${this.onCollTabNav}" id="curated"
class="${this.tabData.view === 'curated' ? '' : 'is-hidden'}">
.active="${isPages}"
currList="${this.tabData.currList === undefined ? "pages" : this.tabData.currList}"
query="${this.tabData.query || ""}"
@coll-tab-nav="${this.onCollTabNav}" id="pages"
class="${isPages ? '' : 'is-hidden'}">
</wr-coll-curated>
<wr-coll-pages .collInfo="${this.collInfo}"
@coll-tab-nav="${this.onCollTabNav}" id="pages"
class="${this.tabData.view === 'pages' ? '' : 'is-hidden'}">
</wr-coll-pages>
<wr-coll-resources .collInfo="${this.collInfo}"
urlSearch="${this.tabData.urlSearch || ""}"
.active="${isResources}"
query="${this.tabData.query || ""}"
urlSearchType="${this.tabData.urlSearchType || ""}"
.currMime="${this.tabData.currMime || ""}"
@coll-tab-nav="${this.onCollTabNav}" id="resources"
class="is-paddingless ${this.tabData.view === 'resources' ? '' : 'is-hidden'}">
class="is-paddingless ${isResources ? '' : 'is-hidden'}">
</wr-coll-resources>
${this.tabData.view === 'replay' ? html`
${isReplay ? html`
<wr-coll-replay .collInfo="${this.collInfo}"
sourceUrl="${this.sourceUrl}"
embed="${this.embed}"
url="${this.tabData.url || ""}"
ts="${this.tabData.ts || ""}"
@coll-tab-nav="${this.onCollTabNav}" id="replay"
class="${this.tabData.view === 'replay' ? '' : 'is-hidden'}">
class="${isReplay ? '' : 'is-hidden'}">
</wr-coll-replay>
` : ``}
`;
+167 -52
View File
@@ -14,23 +14,38 @@ class CuratedLists extends LitElement
constructor() {
super();
this.active = false;
this.collInfo = null;
this.currList = 0;
this.query = "";
this.currList = "pages";
this.lists = [];
this.curatedPages = {};
this.offset = 0;
this.lastST = 0;
this.clickTime = 0;
this.hasNarrative = false;
}
static get properties() {
return {
active: { type: Boolean },
collInfo: { type: Object },
query: { type: String },
lists: { type: Array },
curatedPages: { type: Object },
currList: { type: Number },
currList: { type: String },
hasNarrative: { type: Boolean },
}
}
@@ -42,15 +57,33 @@ class CuratedLists extends LitElement
if (changedProperties.has("collInfo")) {
this.doLoadCurated();
}
if (changedProperties.has("currList") || changedProperties.has("query")) {
this.sendChangeEvent({
currList: this.currList,
query: this.query
});
}
}
async doLoadCurated() {
const resp = await fetch(`${this.collInfo.apiPrefix}/curatedPages?offset=${this.offset}&count=1000`);
this.hasNarrative = this.collInfo.desc || this.collInfo.numLists;
if (!this.collInfo.numLists) {
this.total = 0;
this.curatedPages = {};
this.lists = [];
this.currList = "pages";
return;
}
const resp = await fetch(`${this.collInfo.apiPrefix}/curatedPages?offset=${this.offset}&count=10000`);
const json = await resp.json();
this.total = json.total;
this.curatedPages = {};
this.lists = json.lists;
for (const curated of json.curatedPages) {
if (!this.curatedPages[curated.list]) {
@@ -59,80 +92,150 @@ class CuratedLists extends LitElement
this.curatedPages[curated.list].push(curated);
}
if (this.currList) {
this.scrollToList(this.currList);
}
this.scrollToList();
}
static get styles() {
return wrapCss(css`
:host {
justify-content: flex-start;
align-items: center;
}
.columns {
width: 100vw;
display: flex;
flex-direction: row;
max-height: 100%
align-items: flex-start;
height: 100%;
width: 100%;
justify-self: stretch;
}
.column {
max-height: calc(100% - 70px);
}
#content {
margin-top: 10px;
max-height: calc(100% - 90px);
.column.main-content {
margin: 12px 0px 0px 0px;
padding: 0px;
max-height: calc(100% - 0.75em);
display: flex;
flex-direction: column;
height: min-content;
padding-left: 0.75em;
}
.column.main-content.main-scroll {
padding-right: 0.75em;
word-break: break-all;
}
.column.main-content.pages {
height: calc(100% - 1.2em);
}
ul.menu-list a.is-active {
background-color: #55be6f;
}
:host {
justify-content: flex-start;
align-items: center;
margin-top: 1em;
@media screen and (min-width: 768px) {
.columns {
max-height: 100%;
height: 100%;
margin-top: 0.75em;
}
.column.sidebar {
max-height: 100%;
overflow-y: auto;
}
}
@media screen and (max-width: 767px) {
.columns {
position: relative;
max-height: 100%;
height: 100%;
}
.column.sidebar {
max-height: 150px;
overflow-y: auto;
margin-top: 0.75em;
}
.column.main-content {
position: relative;
overflow-y: auto;
border-top: 1px solid black;
width: 100%;
height: 100%;
max-height: calc(100% - 150px - 0.75em);
}
.menu {
font-size: 0.80rem;
}
}
`);
}
render() {
const currListNum = Number(this.currList);
return html`
<div class="columns">
<div class="column is-one-third">
<div class="menu" style="overflow-y: auto; height: 100%;">
<div class="column sidebar is-one-fifth">
<aside class="menu">
<p class="menu-label">${this.collInfo.title}</p>
<ul class="menu-list">
<li><a class="${this.currList === 'pages' ? 'is-active' : ''}" data-list="all-pages" @click=${this.onClickPages}>Page Search</a></li>
</ul>
${this.hasNarrative ? html`
<ul class="menu-list">
<li>
<a href="#list-0" data-list="0" class="${!this.currList ? 'is-active' : ''}"
@click=${this.onClickScroll}>${this.collInfo.title}</a>
<ul class="menu-list">${this.collInfo.lists.map(list => html`
<a href="#list-0" data-list="0" class="${currListNum === 0 ? 'is-active' : ''}"
@click=${this.onClickScroll}>Narrative</a>
<ul class="menu-list">${this.lists.map(list => html`
<li>
<a @click=${this.onClickScroll} href="#list-${list.id}"
data-list="${list.id}"
class="${this.currList === list.id ? 'is-active' : ''}">${list.title}</a>
class="${currListNum === list.id ? 'is-active' : ''}">${list.title}</a>
</li>`)}
</ul>
</li>
</ul>
</div>
</div>
<div id="content" @scroll=${this.onScroll} class="is-two-thirds column main-scroll">
<section id="list-0" class="container">
<h2 class="title is-3">${this.collInfo.title}</h2>
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''}
</section>
${this.renderContent()}
</ul>` : ``}
</aside>
</div>
${this.renderPages()}
${this.renderLists()}
</div>
`;
}
renderContent() {
renderPages() {
return html`
${this.collInfo.lists.map((list, i) => html`
<div class="column main-content pages ${this.currList === "pages" ? '' : 'is-hidden'}">
<wr-page-view
.active="${this.active && this.currList === "pages"}"
query="${this.query}" .collInfo="${this.collInfo}"
@coll-tab-nav="${this.onCollTabNav}">
</wr-page-view>
</div>
`;
}
onCollTabNav(event) {
if (event.detail.data && event.detail.data.query !== undefined) {
this.query = event.detail.data.query;
}
}
renderLists() {
if (!this.hasNarrative) {
return;
}
return html`
<div @scroll=${this.onScroll} class="${this.currList !== "pages" ? '' : 'is-hidden'} column main-content main-scroll">
<section id="list-0" class="">
<h2 class="has-text-centered title is-3">${this.collInfo.title}</h2>
${this.collInfo.desc ? unsafeHTML(marked(this.collInfo.desc)) : ''}
</section>
${this.lists.map((list, i) => html`
<article id="list-${list.id}">
<div class="content">
<hr/>
@@ -156,6 +259,7 @@ class CuratedLists extends LitElement
</div>
</article>
`)}
</div>
`;
}
@@ -174,25 +278,37 @@ class CuratedLists extends LitElement
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
}
onClickScroll(event) {
onClickPages(event) {
event.preventDefault();
this.scrollToList(event.currentTarget.getAttribute("data-list"));
//this.pageView = true;
this.currList = "pages";
return false;
}
scrollToList(id) {
this.currList = Number(id);
// lists are 1 based, 0 is header, 1 is first list
if (this.currList > this.collInfo.lists.length) {
this.currList = 0;
onClickScroll(event) {
event.preventDefault();
//this.pageView = false;
this.currList = event.currentTarget.getAttribute("data-list");
this.scrollToList();
return false;
}
this.sendChangeEvent({currList: this.currList || undefined});
scrollToList() {
if (this.currList === "pages") {
return;
}
// lists are 1 based, 0 is header, 1 is first list
if (Number(this.currList) > this.lists.length) {
this.currList = "0";
}
const opts = {behavior: "smooth", block: "nearest", inline: "nearest"};
this.clickTime = new Date().getTime();
this.renderRoot.getElementById("list-" + this.currList).scrollIntoView(opts);
const curr = this.renderRoot.getElementById("list-" + this.currList);
if (curr) {
curr.scrollIntoView(opts);
}
}
onScroll(event) {
@@ -219,8 +335,7 @@ class CuratedLists extends LitElement
this.lastST = currST;
if (next && next != curr) {
if (next.id.startsWith("list-")) {
this.currList = Number(next.id.slice(5));
this.sendChangeEvent({currList: this.currList});
this.currList = next.id.slice(5);
}
}
+38 -5
View File
@@ -1,12 +1,18 @@
import { LitElement, html, css } from 'lit-element';
import { registerSW } from './pageutils';
var scriptSrc = document.currentScript && document.currentScript.src;
// ===========================================================================
class EmbedRWP extends LitElement
class Embed extends LitElement
{
constructor() {
super();
this.replayBase = "http://localhost:9990/";
this.replayBase = window.location.origin + "/replay/";
this.swName = __SW_NAME__;
this.view = "replay";
this.ts = "";
this.url = "";
@@ -14,6 +20,9 @@ class EmbedRWP extends LitElement
this.coll = "";
this.paramString = null;
this.deepLink = false;
this.swInited = false;
this.doRegister();
}
static get properties() {
@@ -21,13 +30,17 @@ class EmbedRWP extends LitElement
url: { type: String },
ts: { type: String },
source: { type: String },
replayBase: { type: String },
swName: { type: String },
title: { type: String },
coll: { type: String },
config: { type: String },
swInited: { type: Boolean },
paramString: { type: String },
hashString: { type: String },
@@ -35,6 +48,12 @@ class EmbedRWP extends LitElement
}
}
async doRegister() {
await registerSW(this.swName, this.replayBase);
this.swInited = true;
}
firstUpdated() {
window.addEventListener("message", (event) => {
const iframe = this.renderRoot.querySelector("iframe");
@@ -115,16 +134,30 @@ class EmbedRWP extends LitElement
render() {
return html`
${this.paramString && this.hashString ? html`
<iframe src="${this.replayBase}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"></iframe>
${this.paramString && this.hashString && this.swInited ? html`
<iframe @load="${this.onLoad}" src="${this.replayBase}?${this.paramString}#${this.hashString}" allow="autoplay *; fullscreen"></iframe>
` : html``}
`;
}
onLoad(event) {
if (this.injected) {
return;
}
if (event.target.contentWindow.customElements.get("replay-app-main")) {
return;
}
const script = event.target.contentDocument.createElement("script");
//const script = event.target.contentDocument.querySelector("script");
script.src = scriptSrc;
event.target.contentDocument.head.appendChild(script);
this.injected = true;
}
}
// ===========================================================================
async function main() {
customElements.define("replay-web-page", EmbedRWP);
customElements.define("replay-web-page", Embed);
}
+12 -6
View File
@@ -1,12 +1,20 @@
import { LitElement, html, css } from 'lit-element';
import { wrapCss, dbworker } from './misc';
import { wrapCss } from './misc';
import { parseURLSchemeHostPath } from './pageutils';
let dbworker = null;
// ===========================================================================
class Loader extends LitElement
{
static initDBWorker() {
if (dbworker === null) {
dbworker = new Worker(__SW_NAME__);
}
}
constructor() {
super();
this.progress = 0;
@@ -15,6 +23,8 @@ class Loader extends LitElement
this.coll = "";
this.state = "waiting";
this.loadInfo = null;
Loader.initDBWorker();
}
static get properties() {
@@ -92,11 +102,7 @@ You can select a file to upload from the main page by clicking the \'Choose File
return;
}
source = {
sourceUrl: this.loadInfo.sourceUrl,
loadUrl: this.loadInfo.loadUrl,
name: this.loadInfo.name
}
source = this.loadInfo;
break;
}
} catch (e) {}
+3 -3
View File
@@ -1,3 +1,5 @@
"use strict";
import { LitElement, html, css, unsafeCSS } from 'lit-element';
import { unsafeSVG } from 'lit-html/directives/unsafe-svg';
import { styleMap } from 'lit-html/directives/style-map';
@@ -15,8 +17,6 @@ function wrapCss(custom) {
const IS_APP = window.electron && window.electron.IS_APP || window.matchMedia('(display-mode: standalone)').matches;
const dbworker = new Worker(__SW_PATH__);
// ===========================================================================
class FaIcon extends LitElement
@@ -101,4 +101,4 @@ class AnimLogo extends FaIcon
customElements.define("fa-icon", FaIcon);
customElements.define("wr-anim-logo", AnimLogo);
export { wrapCss, IS_APP, dbworker, rwpLogo, FaIcon, AnimLogo };
export { wrapCss, IS_APP, rwpLogo, FaIcon, AnimLogo };
+313 -18
View File
@@ -1,8 +1,16 @@
"use strict";
import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc';
import { getTS } from './pageutils';
import FlexSearch from 'flexsearch';
import "keyword-mark-element/lib/keyword-mark.js";
//const FlexSearch = Object;
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
// ===========================================================================
class Pages extends LitElement
@@ -10,55 +18,337 @@ class Pages extends LitElement
constructor() {
super();
this.pages = [];
this.filteredPages = [];
this.sortedPages = [];
this.query = "";
this.flex = null;
this.newQuery = null;
this.loading = false;
this.active = false;
this.changeNeeded = false;
}
static get sortKeys() {
return [
{
"key": "title",
"name": "Title"
},
{
"key": "ts",
"name": "Date"
}
];
}
static get properties() {
return {
active: { type: Boolean},
collInfo: { type: Object },
pages: { type: Array }
pages: { type: Array },
filteredPages: { type: Array },
sortedPages: { type: Array },
query: { type: String },
loading: { type: Boolean },
}
}
firstUpdated() {
//this._ival = window.setInterval(() => {
//}, 10000);
}
_timedUpdate() {
if (this.newQuery !== null) {
this.query = this.newQuery;
this.newQuery = null;
this.filter();
}
}
updated(changedProperties) {
if (changedProperties.has("collInfo")) {
this.doUpdate();
} else if (changedProperties.has("query")) {
this.filter();
}
if (changedProperties.has("active") && this.active) {
if (this.changeNeeded) {
this.filter();
}
}
}
async doUpdate() {
const result = await fetch(`${this.collInfo.apiPrefix}/pages`);
const json = await result.json();
console.log(json);
this.pages = json.pages || [];
this.updateTextSearch();
this.filter();
}
onChangeQuery(event) {
this.newQuery = event.currentTarget.value;
//this.loading = true;
if (this._ival) {
window.clearTimeout(this._ival);
}
this._ival = window.setTimeout(() => this._timedUpdate(), 250);
}
async filter() {
if (this.loading) {
return;
}
if (!this.active) {
this.changeNeeded = true;
}
this.loading = true;
if (this.flex && this.query) {
const result = await this.flex.search(this.query);
this.filteredPages = result;
} else {
this.filteredPages = this.pages;
}
this.loading = false;
this.changeNeeded = false;
const data = {query: this.query};
this.sendChangeEvent(data);
}
sendChangeEvent(data) {
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
}
updateTextSearch() {
this.flex = new FlexSearch({
doc: {
id: "id",
field: ["url", "title", "text"],
},
async: true
});
this.flex.add(this.pages);
}
static get styles() {
return wrapCss(css`
.main.section {
:host {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 0 0.75rem 0.75rem 0;
}
.scroller {
overflow-y: auto;
padding: 3em;
display: flex;
flex-direction: column;
flex: auto;
height: 100%;
}
.panel-block:last-child {
border-bottom: 1px solid rgb(237, 237, 237);
}
.panel-block {
border-left: 1px solid rgb(237, 237, 237);
border-right: 1px solid rgb(237, 237, 237);
}
.panel-block.status {
justify-content: space-between;
}
nav.panel {
box-shadow: none;
display: flex;
flex-direction: column;
flex: 1;
height: 100%;
}
.light-blue {
background-color: #43daff;
}
`);
}
render() {
return html`
<div class="main section">
${this.pages.map((p) => html`
<nav class="panel">
<div class="panel-heading light-blue">Pages for: ${this.collInfo.title}</div>
<div class="panel-block">
<div class="control has-icons-left ${this.loading ? 'is-loading' : ''}">
<input type="text" 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}"/></span>
</div>
</div>
<div class="panel-block status">
<span class="num-results">${this.formatResults()}</span>
<wr-sorter id="pages"
.sortKeys="${Pages.sortKeys}"
.data="${this.filteredPages}"
@sort-changed="${(e) => this.sortedPages = e.detail.sortedData}"
class="${this.filteredPages.length ? '' : 'is-hidden'}">
</wr-sorter>
</div>
<div class="scroller" @scroll="${this.onScroll}">
${this.sortedPages.map((p) => html`
<div class="panel-block"><wr-page-entry query="${this.query}" .page="${p}"></wr-page-entry></div>`)}
</div>
</nav>
`;
}
formatResults() {
if (!this.collInfo || !this.collInfo.numPages) {
return html`No Pages defined this archive. Check out <a href="#view=resources">Page Resources</a> to search by URL.`;
}
if (this.loading) {
return "Searching...";
}
if (!this.sortedPages.length) {
return "No Pages Found."
} else if (this.sortedPages.length === 1) {
return "1 Page";
} else {
return `${this.sortedPages.length} Pages`;
}
}
onScroll(event) {
const element = event.currentTarget;
const diff = (element.scrollHeight - element.scrollTop) - element.clientHeight;
if (diff < 40) {
const sorter = this.renderRoot.querySelector("wr-sorter");
if (sorter) {
sorter.getMore();
}
}
}
}
// ===========================================================================
class PageEntry extends LitElement
{
constructor() {
super();
this.query = "";
this.textSnippet = "";
this.page = null;
}
static get properties() {
return {
query: { type: String },
textSnippet: { type: String },
page: { type: Object }
}
}
static get styles() {
return wrapCss(css`
:host {
min-height: min-content;
width: 100%;
word-break: break-all;
}
a {
word-break: break-all;
}
@media screen and (max-width: 767px) {
.col-date div {
display: inline;
}
.columns {
display: flex;
flex-direction: column-reverse;
}
}
`);
}
updated(changedProperties) {
if (changedProperties.has("page") || changedProperties.has("query")) {
this.updateSnippet();
}
}
render() {
const p = this.page;
const date = new Date(p.ts);
return html`
<div class="columns">
<div class="column is-one-fifth">
${new Date(p.date).toLocaleString()}
<div class="column col-date is-one-fifth">
<div>${date.toLocaleDateString()}</div>
<div>${date.toLocaleTimeString()}</div>
</div>
<div class="column">
<a @click="${this.onReplay}" data-url="${p.url}" data-ts="${getTS(p.date)}" href="#">
<p class="is-size-6 has-text-weight-bold has-text-link">${p.title || p.url}</p>
<p class="has-text-dark">${p.url}</p>
<a @click="${this.onReplay}" data-url="${p.url}" data-ts="${getTS(date.toISOString())}" href="#">
<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>
</a>
${this.textSnippet ? html`
<div class="text"><keyword-mark keywords="${this.query}">${this.textSnippet}</keyword-mark></div>` : html``}
</div>
</div>`
)}
</div>
`;
</div>`;
}
updateSnippet() {
const oldVal = this.textSnippet;
if (!this.query || !this.page.text) {
this.textSnippet = null;
this.requestUpdate('textSnippet', oldVal);
return;
}
let textContent = this.page.text;
let query = this.query;
let inx = textContent.indexOf(this.query);
if (inx < 0) {
let textLower = textContent.toLowerCase();
let queryLower = query.toLowerCase();
inx = textLower.indexOf(queryLower);
if (inx < 0) {
this.textSnippet = null;
this.requestUpdate('textSnippet', oldVal);
return;
}
textContent = textLower;
query = queryLower;
}
let lastInx = textContent.lastIndexOf(query);
inx = Math.max(inx - 100, 0);
lastInx = Math.min(lastInx + 200, textContent.length);
if (inx === 0 && lastInx === textContent.length) {
this.textSnippet = textContent;
} else {
this.textSnippet = "..." + textContent.slice(inx, lastInx) + "...";
}
this.requestUpdate('textSnippet', oldVal);
}
onReplay(event) {
@@ -68,11 +358,16 @@ class Pages extends LitElement
ts: event.currentTarget.getAttribute("data-ts"),
view: "replay"
};
this.dispatchEvent(new CustomEvent("coll-tab-nav", {detail: {data}}));
this.sendChangeEvent(data);
return false;
}
sendChangeEvent(data) {
this.dispatchEvent(new CustomEvent("coll-tab-nav", {bubbles: true, composed: true, detail: {data}}));
}
}
customElements.define("wr-coll-pages", Pages);
customElements.define("wr-page-view", Pages);
customElements.define("wr-page-entry", PageEntry);
export { Pages };
export { Pages, PageEntry };
+27 -4
View File
@@ -1,3 +1,29 @@
import { register } from 'register-service-worker';
async function registerSW(name = "sw.js", scope = "./") {
let resolve, reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
register(scope + name, {
registrationOptions: { scope },
registered (reg) {
console.log('Service worker is registered');
resolve();
},
error (error) {
console.error('Error during service worker registration:', error);
reject();
}
});
await p;
}
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
@@ -71,7 +97,4 @@ function parseURLSchemeHostPath(url) {
}
export { digestMessage, tsToDate, getTS, sourceToId, parseURLSchemeHostPath };
export { digestMessage, tsToDate, getTS, sourceToId, parseURLSchemeHostPath, registerSW };
+29 -6
View File
@@ -94,7 +94,7 @@ class Replay extends LitElement
}
if (this.iframeUrl && changedProperties.has("iframeUrl")) {
this.isLoading = true;
this.waitForLoad();
}
if (this.replayUrl && changedProperties.has("replayUrl")) {
@@ -123,7 +123,7 @@ class Replay extends LitElement
this.replayTS = event.data.ts;
this.replayUrl = event.data.url;
this.title = event.data.title || this.title;
this.isLoading = false;
this.clearLoading();
if (event.data.icons) {
const icons = event.data.icons;
@@ -155,11 +155,30 @@ class Replay extends LitElement
const iframe = this.renderRoot.querySelector("iframe");
if (iframe) {
this.isLoading = true;
this.waitForLoad();
iframe.contentWindow.location.reload();
}
}
waitForLoad() {
this.isLoading = true;
this._loadPoll = window.setInterval(() => {
const iframe = this.renderRoot.querySelector("iframe");
if (!iframe || !iframe.contentDocument || !iframe.contentWindow ||
(iframe.contentDocument.readyState === "complete" && !iframe.contentWindow._WBWombat)) {
this.clearLoading();
}
}, 5000);
}
clearLoading() {
this.isLoading = false;
if (this._loadPoll) {
window.clearInterval(this._loadPoll);
this._loadPoll = null;
}
}
static get styles() {
return wrapCss(css`
host: {
@@ -206,6 +225,7 @@ class Replay extends LitElement
.intro-panel .panel-block {
padding: 1.0em;
flex-direction: column;
}
#datetime {
@@ -239,13 +259,13 @@ class Replay extends LitElement
<fa-icon size="1.0em" class="has-text-grey" .svg="${this.isFullscreen ? fasUnfullscreen : fasFullscreen}"></fa-icon>
</span>
</a>
<a id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : ''}" @click="${this.onRefresh}">
<button id="refresh" class="button is-borderless ${this.isLoading ? 'is-loading' : ''}" @click="${this.onRefresh}">
<span class="icon is-small">
${!this.isLoading ? html`
<fa-icon size="1.0em" class="has-text-grey" .svg="${fasRefresh}"></fa-icon>
` : ``}
</span>
</a>
</button>
<p class="control is-expanded">
<input id="url" class="input" type="text" .value="${this.replayUrl}" placeholder="https://... Enter a URL to replay from the archive here">
</p>
@@ -261,7 +281,10 @@ class Replay extends LitElement
` : html`
<nav class="panel intro-panel">
<p class="panel-heading">Replay Web Page</p>
<p class="panel-block">Enter a URL above to replay it from the web archive!</p>
<div class="panel-block">
<p>Enter a URL above to replay it from the web archive!</p>
<p>(Check out the <a href="#view=pages">Pages</a> or <a href="#view=resources">Page Resources</a> to find URLs in this archive.)</p>
</div>
</nav>
`}
+119
View File
@@ -0,0 +1,119 @@
import { LitElement, html, css } from 'lit-element';
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';
const DEFAULT_RESULTS = 100;
// ===========================================================================
class Sorter extends LitElement
{
constructor() {
super();
this.sortedData = [];
this.data = [];
this.numResults = DEFAULT_RESULTS;
}
static get properties() {
return {
id: { type: String },
defaultKey: {type: String },
//numResults: {type: Number},
data: { type: Array },
sortedData: { type: Array },
sortKey: { type: String },
sortDesc: { type: Boolean },
}
}
firstUpdated() {
this.sortKey = localStorage.getItem(`${this.id}:sortKey`) || this.defaultKey;
this.sortDesc = localStorage.getItem(`${this.id}:sortDesc`) === "1";
}
updated(changedProperties) {
const keyChanged = changedProperties.has("sortKey");
const descChanged = changedProperties.has("sortDesc");
const dataChanged = changedProperties.has("data");
const numResultsChanged = changedProperties.has("numResults");
if (keyChanged) {
localStorage.setItem(`${this.id}:sortKey`, this.sortKey);
}
if (descChanged) {
localStorage.setItem(`${this.id}:sortDesc`, this.sortDesc ? "1" : "0");
}
if (keyChanged || descChanged || dataChanged) {
this.sortData();
}
if (dataChanged) {
}
//if (numResultsChanged && !dataChanged) {
//this.sendSortChanged();
//}
}
sortData() {
this.sortedData = [...this.data];
this.numResults = DEFAULT_RESULTS;
this.sortedData.sort((first, second) => {
if (first[this.sortKey] === second[this.sortKey]) {
return 0;
}
return (this.sortDesc == (first[this.sortKey] < second[this.sortKey])) ? 1 : -1;
});
this.sendSortChanged();
}
sendSortChanged() {
const detail = {sortedData: this.sortedData.slice(0, this.numResults)};
this.dispatchEvent(new CustomEvent("sort-changed", {detail}));
}
getMore(more = 100) {
if (this.numResults >= this.sortedData.length) {
return;
}
this.numResults += more;
this.sendSortChanged();
}
static get styles() {
return wrapCss(css`
:host {
min-width: 100px;
}
`);
}
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>
</div>
<button @click=${(e) => this.sortDesc = !this.sortDesc} class="button is-small">
<span>Order:</span>
<span class="icon"><fa-icon .svg=${this.sortDesc ? fasSortUp : fasSortDown}></span>
</button>`;
}
}
customElements.define("wr-sorter", Sorter);
export { Sorter };
+21
View File
@@ -0,0 +1,21 @@
"use strict";
import INDEX_HTML from '../index.html';
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 + "ui.js", {type: "application/javascript", content: UI_JS});
const { SWReplay } = require('wabac/src/swmain');
self.sw = new SWReplay(staticData);
} else if (self.postMessage) {
// Inited as Web Worker
const { WorkerLoader } = require('wabac/src/loaders');
const loader = new WorkerLoader(self);
}
+3 -29
View File
@@ -1,4 +1,5 @@
import { App } from './appmain';
import { Chooser } from './chooser';
import { CollIndex } from './coll-index';
import { Coll } from './coll';
import { CuratedList } from './curated-lists';
@@ -6,34 +7,7 @@ import { GDrive } from './gdrive';
import { Loader } from './loader';
import { Pages } from './pages';
import { Replay } from './replay';
import { Sorter } from './sorter';
import { URLResources } from './url-resources';
import { Embed } from './embed';
import { register } from 'register-service-worker';
// ===========================================================================
// ===========================================================================
async function main(url) {
let resolve, reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
register(url, {
registrationOptions: { scope: './' },
ready (registration) {
console.log('Service worker is active.');
resolve();
},
error (error) {
console.error('Error during service worker registration:', error);
reject();
}
});
await p;
}
main(__SW_PATH__);
+26 -13
View File
@@ -1,6 +1,8 @@
import { LitElement, html, css } from 'lit-element';
import { wrapCss } from './misc';
import fasSearch from '@fortawesome/fontawesome-free/svgs/solid/search.svg';
// ===========================================================================
class URLResources extends LitElement
@@ -26,13 +28,15 @@ class URLResources extends LitElement
this.collInfo = null;
this.currMime = "";
this.urlSearch = "";
this.query = "";
this.urlSearchType = "";
this.filteredResults = [];
this.results = [];
this.newQuery = null;
this.tryMore = false;
this.loading = false;
@@ -44,7 +48,7 @@ class URLResources extends LitElement
return {
collInfo: { type: Object },
currMime: { type: String },
urlSearch: { type: String },
query: { type: String },
urlSearchType: { type: String },
filteredResults: { type: Array },
loading: { type: Boolean },
@@ -58,14 +62,21 @@ class URLResources extends LitElement
//this.doLoadResources();
}
_timedUpdate() {
if (this.newQuery !== null) {
this.query = this.newQuery;
this.newQuery = null;
}
}
updated(changedProperties) {
if (changedProperties.has("urlSearch") ||
if (changedProperties.has("query") ||
changedProperties.has("urlSearchType") ||
changedProperties.has("currMime")) {
this.doLoadResources();
const data = {
urlSearch: this.urlSearch,
query: this.query,
urlSearchType: this.urlSearchType,
currMime: this.currMime
};
@@ -90,7 +101,7 @@ class URLResources extends LitElement
}
this.loading = true;
let url = (this.urlSearchType !== "" ? this.urlSearch : "");
let url = (this.urlSearchType !== "" ? 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://
@@ -139,22 +150,23 @@ class URLResources extends LitElement
onChangeTypeSearch(event) {
this.currMime = event.currentTarget.value;
//this.doLoadResources();
}
onChangeUrlSearch(event) {
this.urlSearch = event.currentTarget.value;
//this.doLoadResources();
onChangeQuery(event) {
this.newQuery = event.currentTarget.value;
if (this._ival) {
window.clearTimeout(this._ival);
}
this._ival = window.setTimeout(() => this._timedUpdate(), 250);
}
onClickUrlType(event) {
this.urlSearchType = event.currentTarget.value;
//this.doLoadResources();
}
filter() {
const filteredResults = [];
const filterText = (this.urlSearchType === "" ? this.urlSearch : "");
const filterText = (this.urlSearchType === "" ? this.query : "");
for (const result of this.results) {
if (!filterText || result.url.indexOf(filterText) >= 0) {
filteredResults.push(result);
@@ -270,8 +282,9 @@ class URLResources extends LitElement
</select>
</div>
<div class="field flex-auto">
<div class="control ${this.loading ? 'is-loading' : ''}">
<input type="text" class="input" @input="${this.onChangeUrlSearch}" value="${this.urlSearch}" type="text" placeholder="Enter URL search here">
<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>
</div>
+4 -4
View File
File diff suppressed because one or more lines are too long
+573 -410
View File
File diff suppressed because one or more lines are too long
+3 -7
View File
@@ -15,9 +15,6 @@ const HELPER_PROXY = "https://helper-proxy.webrecorder.workers.dev";
const GDRIVE_CLIENT_ID = "160798412227-tko4c82uopud11q105b2lvbogsj77hlg.apps.googleusercontent.com";
const electronMainConfig = (env, argv) => {
return {
target: 'electron-main',
@@ -65,8 +62,7 @@ const browserConfig = (env, argv) => {
mode: 'production',
entry: {
'ui': './src/ui.js',
'embed': './src/embed.js',
'sw': 'wabac/src/sw.js'
'sw': './src/sw.js'
},
output: {
@@ -92,7 +88,7 @@ const browserConfig = (env, argv) => {
plugins: [
new MiniCssExtractPlugin(),
new webpack.DefinePlugin({
__SW_PATH__: JSON.stringify('./sw.js'),
__SW_NAME__: JSON.stringify('sw.js'),
__APP_FILE_SERVE_PREFIX__ : JSON.stringify(APP_FILE_SERVE_PREFIX),
__HELPER_PROXY__ : JSON.stringify(HELPER_PROXY),
__GDRIVE_CLIENT_ID__ : JSON.stringify(GDRIVE_CLIENT_ID)
@@ -114,7 +110,7 @@ const browserConfig = (env, argv) => {
loaders: ['to-string-loader', 'css-loader', 'sass-loader']
},
{
test: /wombat.js|wombatWorkers.js$/i,
test: /wombat.js|wombatWorkers.js|index.html$/i,
loaders: ['raw-loader'],
}
]
+10
View File
@@ -2130,6 +2130,11 @@ flatpickr@^4.6.1:
resolved "https://registry.yarnpkg.com/flatpickr/-/flatpickr-4.6.3.tgz#15a8b76b6e34e3a072861250503a5995b9d3bc60"
integrity sha512-007VucCkqNOMMb9ggRLNuJowwaJcyOh4sKAFcdGfahfGc7JQbf94zSzjdBq/wVyHWUEs5o3+idhFZ0wbZMRmVQ==
flexsearch@^0.6.32:
version "0.6.32"
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.6.32.tgz#1e20684d317af65baa445cdd9864a5f5b320f510"
integrity sha512-EF1BWkhwoeLtbIlDbY/vDSLBen/E5l/f1Vg7iX5CDymQCamcx1vhlc3tIZxIDplPjgi0jhG37c67idFbjg+v+Q==
flush-write-stream@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -3198,6 +3203,11 @@ keyv@^3.0.0:
dependencies:
json-buffer "3.0.0"
keyword-mark-element@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/keyword-mark-element/-/keyword-mark-element-0.1.2.tgz#b57e025bde18b0ab1d3872d5c06ed2ec6eca4fd4"
integrity sha512-kU+wMFIczvsmMg9QXi1rJ1VLYK4IpI+Jud9vBMzo7Zt9Nvx9nTsZJFHuH9xvlQxqHAcyQvQyv0j6kfRAlSn0oA==
killable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"