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:
+129
-199
@@ -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} </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} </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 };
|
||||
Reference in New Issue
Block a user