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
+129 -199
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>
</div>
</form>
</div>
</nav>
</section>
<section class="section no-top-padding">
<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>
<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">
<a href="?source=${coll.sourceUrl}">${coll.title || coll.filename}</a>
</td>
<td class="col-source"><span class="source-text">${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">
${!this._deleting[coll.sourceUrl] ? html`
<button data-coll-index="${i}" @click="${this.onDeleteColl}" class="delete"></button>
` : html`
<button class="button is-loading is-static"></button>`}
</td>
</tr>
`)}
</tbody>
</table>
</div>` : html`
${this.sortedColls.length ? html`
${this.sortedColls.map((coll, i) => html`
<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>
</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>` : ''}
</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 class="delete" title="Unload Collection" data-coll-index="${i}" @click="${this.onDeleteColl}"></button>
` : html`
<button class="button delete is-loading is-static"></button>`}
</div>
`)}
</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,18 +266,9 @@ 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);
export { CollIndex };