improve typescript and linting usage across codebase
add ts-lit-plugin and eslint-typescript disable no-invalid-css rule fix a bunch of errors raised by eslint and typescript run prettier & eslint on the remaining files remove unused imports split coll-index into index and info components... and rename "coll" -> "item" for these. also includes some fixes for previously-undocumented properties used on non-ts elements. extract labeled fields into separate component... and make a few type improvements too swap custom copy button for shoelace one rename a few more missed things
This commit is contained in:
+10
-7
@@ -1,5 +1,5 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import {
|
||||
wrapCss,
|
||||
@@ -13,6 +13,9 @@ import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.sv
|
||||
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";
|
||||
import "./item";
|
||||
import "./item-index";
|
||||
import "./chooser";
|
||||
|
||||
// ===========================================================================
|
||||
class ReplayWebApp extends LitElement {
|
||||
@@ -157,7 +160,7 @@ class ReplayWebApp extends LitElement {
|
||||
min-width: 0px;
|
||||
flex-direction: column;
|
||||
}
|
||||
wr-coll {
|
||||
wr-item {
|
||||
height: 100%;
|
||||
}
|
||||
.navbar {
|
||||
@@ -379,7 +382,7 @@ class ReplayWebApp extends LitElement {
|
||||
}
|
||||
|
||||
renderColl() {
|
||||
return html` <wr-coll
|
||||
return html` <wr-item
|
||||
.loadInfo="${this.loadInfo}"
|
||||
sourceUrl="${this.sourceUrl || ""}"
|
||||
embed="${this.embed}"
|
||||
@@ -390,11 +393,11 @@ class ReplayWebApp extends LitElement {
|
||||
@update-title=${this.onTitle}
|
||||
@coll-loaded=${this.onCollLoaded}
|
||||
@about-show=${() => (this.showAbout = true)}
|
||||
></wr-coll>`;
|
||||
></wr-item>`;
|
||||
}
|
||||
|
||||
renderHomeIndex() {
|
||||
return html` <wr-coll-index>
|
||||
return html` <wr-item-index>
|
||||
${!IS_APP
|
||||
? html`
|
||||
<p slot="header" class="tagline is-size-5 has-text-centered">
|
||||
@@ -412,7 +415,7 @@ class ReplayWebApp extends LitElement {
|
||||
@did-drop-file="${() => (this.droppedFile = null)}"
|
||||
@load-start=${this.onStartLoad}
|
||||
></wr-chooser>
|
||||
</wr-coll-index>`;
|
||||
</wr-item-index>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -763,7 +766,7 @@ class ReplayWebApp extends LitElement {
|
||||
<summary>Legalese:</summary>
|
||||
<p style="font-size: 0.8rem">DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE"
|
||||
AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE,
|
||||
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
|
||||
INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.</p>
|
||||
</details>
|
||||
<div class="has-text-centered">
|
||||
<a class="button is-warning" href="#" @click="${
|
||||
|
||||
+2
-1
@@ -37,7 +37,8 @@ class Chooser extends LitElement {
|
||||
static get properties() {
|
||||
return {
|
||||
fileDisplayName: { type: String },
|
||||
droppedFile: { type: File },
|
||||
/** @type File */
|
||||
droppedFile: { attribute: false },
|
||||
newFullImport: { type: Boolean },
|
||||
noHead: { type: Boolean },
|
||||
};
|
||||
|
||||
@@ -1,625 +0,0 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { wrapCss, apiPrefix } from "./misc";
|
||||
import { map } from "lit/directives/map.js";
|
||||
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
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";
|
||||
|
||||
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
|
||||
|
||||
import type { Coll } from "./types";
|
||||
|
||||
// ===========================================================================
|
||||
class CollIndex extends LitElement {
|
||||
@property({ type: Array })
|
||||
colls: Coll[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
query = "";
|
||||
|
||||
@property({ type: Array })
|
||||
filteredColls: any[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
sortedColls: any[] | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hideHeader: any = null;
|
||||
|
||||
@property({ type: String })
|
||||
dateName = "Date Loaded";
|
||||
|
||||
@property({ type: String })
|
||||
headerName = "Loaded Archives";
|
||||
|
||||
@state()
|
||||
private _deleting: any = {};
|
||||
|
||||
private typeFilter = "";
|
||||
private indexParams = "";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.hideHeader = localStorage.getItem("index:hideHeader") === "1";
|
||||
}
|
||||
|
||||
get sortKeys() {
|
||||
return [
|
||||
{ key: "title", name: "Title" },
|
||||
|
||||
{ key: "sourceUrl", name: "Source" },
|
||||
|
||||
{ key: "ctime", name: this.dateName },
|
||||
|
||||
{ key: "size", name: "Total Size" },
|
||||
];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.loadColls();
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("hideHeader")) {
|
||||
localStorage.setItem("index:hideHeader", this.hideHeader ? "1" : "0");
|
||||
}
|
||||
if (changedProperties.has("colls") || changedProperties.has("query")) {
|
||||
this.filter();
|
||||
}
|
||||
}
|
||||
|
||||
filter() {
|
||||
if (!this.query) {
|
||||
this.filteredColls = this.colls;
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
this.filteredColls.push(coll);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadColls() {
|
||||
const resp = await fetch(`${apiPrefix}/coll-index?${this.indexParams}`);
|
||||
try {
|
||||
if (resp.status !== 200) {
|
||||
throw new Error("Invalid API Response, Retry");
|
||||
}
|
||||
const json = await resp.json();
|
||||
this.colls = json.colls.map((coll) => {
|
||||
coll.title = coll.title || coll.filename;
|
||||
return coll;
|
||||
});
|
||||
|
||||
this._deleting = {};
|
||||
this.sortedColls = [];
|
||||
} catch (e) {
|
||||
// likely no sw registered yet, or waiting for new sw to register, retry again
|
||||
setTimeout(() => this.loadColls(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
async onDeleteColl(event) {
|
||||
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]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._deleting[coll.sourceUrl] = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const resp = await fetch(`${apiPrefix}/c/${coll.id}`, { method: "DELETE" });
|
||||
if (resp.status === 200) {
|
||||
const json = await resp.json();
|
||||
this.colls = json.colls;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(CollIndex.compStyles);
|
||||
}
|
||||
|
||||
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: 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;
|
||||
}
|
||||
|
||||
.panel-color {
|
||||
background-color: rgb(210, 249, 214);
|
||||
}
|
||||
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
return html`<h2 class="panel-heading panel-color">
|
||||
<span>${this.headerName}</span>
|
||||
</h2>`;
|
||||
}
|
||||
|
||||
renderSearchHeader() {
|
||||
return "";
|
||||
}
|
||||
|
||||
render() {
|
||||
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}"
|
||||
placeholder="Search by Archive Title or Source"
|
||||
/>
|
||||
<span class="icon is-left is-small">
|
||||
<fa-icon .svg="${fasSearch}"></fa-icon>
|
||||
</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` <span
|
||||
class="button delete-button is-loading is-static"
|
||||
>
|
||||
Deleting
|
||||
</span>`}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="panel-block extra-padding">
|
||||
${this.sortedColls === null
|
||||
? html`<i>Loading Archives...</i>`
|
||||
: this.renderEmpty()}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderCollInfo(coll: Coll) {
|
||||
return html`<wr-coll-info .coll=${coll}></wr-coll-info>`;
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
return html`<i
|
||||
>No Archives so far! Archives loaded in the section above will appear
|
||||
here.</i
|
||||
>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
class CollInfo extends LitElement {
|
||||
@property({ type: Object })
|
||||
coll!: Coll | Record<string, never>;
|
||||
|
||||
@property({ type: Boolean })
|
||||
detailed = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
canDelete = false;
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(CollInfo.compStyles);
|
||||
}
|
||||
|
||||
static get compStyles() {
|
||||
return css`
|
||||
.columns {
|
||||
width: 100%;
|
||||
}
|
||||
.column {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
:host(.is-list) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host(.is-list) .column {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.col-title:hover {
|
||||
}
|
||||
.col-title a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.column:hover > .copy,
|
||||
.col-content:hover .copy,
|
||||
.copy:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: -4px 0 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.col-content {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
renderSource() {
|
||||
const coll = this.coll;
|
||||
return html`
|
||||
<div class="column is-4">
|
||||
<p class="minihead">Source</p>
|
||||
<div class="col-content">
|
||||
${coll.sourceUrl &&
|
||||
(coll.sourceUrl.startsWith("http://") ||
|
||||
coll.sourceUrl.startsWith("https://"))
|
||||
? html` <a href="${coll.sourceUrl}">${coll.sourceUrl}</a> `
|
||||
: html` ${coll.sourceUrl}`}
|
||||
${coll.sourceUrl && coll.sourceUrl.startsWith("googledrive://")
|
||||
? html` <i>(${coll.filename})</i>`
|
||||
: ""}
|
||||
<a @click="${(e) => this.onCopy(e, coll.sourceUrl)}" class="copy">
|
||||
<fa-icon .svg="${fasCopy}"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<p class="minihead">Collection id</p>
|
||||
<div class="col-content">
|
||||
${coll.coll}
|
||||
<a @click="${(e) => this.onCopy(e, coll.coll)}" class="copy">
|
||||
<fa-icon .svg="${fasCopy}"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<p class="minihead">Date Loaded</p>
|
||||
<div class="col-content">
|
||||
${coll.ctime ? new Date(coll.ctime).toLocaleString() : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-2">
|
||||
<p class="minihead">Total Size</p>
|
||||
<div class="col-content">
|
||||
${prettyBytes(Number(coll.totalSize || coll.size || 0))}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderSummaryView() {
|
||||
const coll = this.coll;
|
||||
|
||||
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.name || coll.title || coll.filename}</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
${this.renderSource()}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderDetailed() {
|
||||
const coll = this.coll;
|
||||
|
||||
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}`
|
||||
: "";
|
||||
|
||||
return html` <div class="columns">
|
||||
${coll.name || coll.title
|
||||
? html`<div class="column">
|
||||
<p class="minihead">Title</p>
|
||||
<div class="col-content">${coll.name || coll.title}</div>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="column">
|
||||
<p class="minihead">Filename</p>
|
||||
<div class="col-content">${coll.filename}</div>
|
||||
</div>
|
||||
${coll.resources
|
||||
? html`<div class="column">
|
||||
<p class="minihead">Files</p>
|
||||
<ol style="padding: revert">
|
||||
${map(
|
||||
coll.resources,
|
||||
(resource: any) =>
|
||||
html`<li>
|
||||
<a href="${resource.path}">${resource.name + "\n"}</a>
|
||||
</li>`,
|
||||
)}
|
||||
</ol>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.renderSource()}
|
||||
${domain
|
||||
? html`
|
||||
<div class="column">
|
||||
<p class="minihead">Observed By</p>
|
||||
<span class="col-content">
|
||||
<p>${domain}</p>
|
||||
${certFingerprintUrl
|
||||
? html`<span
|
||||
><a target="_blank" href="${certFingerprintUrl}"
|
||||
>View Certificate</a
|
||||
></span
|
||||
>`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: software
|
||||
? html`
|
||||
<div class="column">
|
||||
<p class="minihead">Created With</p>
|
||||
<div class="col-content">${software || "Unknown"}</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Validation</p>
|
||||
<div class="col-content">
|
||||
${numValid > 0 || numInvalid > 0
|
||||
? html` <p>
|
||||
${numValid} hashes
|
||||
verified${numInvalid ? html`, ${numInvalid} invalid` : ""}
|
||||
</p>`
|
||||
: html` Not Available`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Package Hash</p>
|
||||
<div class="col-content">
|
||||
${datapackageHash || "Not Available"}
|
||||
<a @click="${(e) => this.onCopy(e, datapackageHash)}" class="copy">
|
||||
<fa-icon .svg="${fasCopy}"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Observer Public Key</p>
|
||||
<div class="col-content">
|
||||
${publicKey || "Not Available"}
|
||||
<a @click="${(e) => this.onCopy(e, publicKey)}" class="copy">
|
||||
<fa-icon .svg="${fasCopy}"></fa-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column">
|
||||
<p class="minihead">Loading Mode</p>
|
||||
<div class="col-content">
|
||||
${coll.onDemand ? "Download On-Demand" : "Fully Local"}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.detailed ? this.renderDetailed() : this.renderSummaryView();
|
||||
}
|
||||
|
||||
onCopy(event: Event, text: string | undefined) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (text) navigator.clipboard.writeText(text);
|
||||
return false;
|
||||
}
|
||||
|
||||
onPurge(reload) {
|
||||
const detail = { reload };
|
||||
this.dispatchEvent(new CustomEvent("coll-purge", { detail }));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-coll-info", CollInfo);
|
||||
customElements.define("wr-coll-index", CollIndex);
|
||||
|
||||
export { CollIndex, CollInfo };
|
||||
@@ -0,0 +1,57 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { wrapCss } from "./../misc";
|
||||
import "@shoelace-style/shoelace/dist/components/copy-button/copy-button.js";
|
||||
|
||||
@customElement("wr-labeled-field")
|
||||
class LabeledField extends LitElement {
|
||||
@property({ type: String })
|
||||
copy?: string;
|
||||
|
||||
@property({ type: String })
|
||||
label?: string;
|
||||
|
||||
@property({ type: String })
|
||||
class?: string;
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(LabeledField.compStyles);
|
||||
}
|
||||
|
||||
static get compStyles() {
|
||||
return css`
|
||||
:host {
|
||||
min-width: unset; /* @todo(emma, 2023-11-06) see about removing this, if the min-width set on all web components is unnecessary */
|
||||
}
|
||||
|
||||
/* @todo(emma, 2023-11-06) add option for monospace treatment, rather than making everything mono. this could also be a class on the host element? */
|
||||
.col-content {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`${this.label
|
||||
? html`<p class="minihead">${this.label}</p>`
|
||||
: nothing}
|
||||
<div class="col-content">
|
||||
<slot></slot>
|
||||
${this.copy
|
||||
? html` <sl-copy-button .value=${this.copy || ""}></sl-copy-button>`
|
||||
: nothing}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export { LabeledField };
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { CollectionLoader } from "@webrecorder/wabac/src/loaders";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { ipcRenderer, contextBridge } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("electron", { IS_APP: true });
|
||||
|
||||
@@ -466,6 +466,7 @@ class ElectronReplayApp {
|
||||
}
|
||||
|
||||
getOpenUrl(argv) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
argv = require("minimist")(argv.slice(process.defaultApp ? 2 : 1));
|
||||
|
||||
const filename =
|
||||
|
||||
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
declare module "*.svg";
|
||||
declare var __SW_NAME__: string;
|
||||
declare let __SW_NAME__: string;
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { wrapCss, apiPrefix } from "./misc";
|
||||
|
||||
import fasArrowUp from "@fortawesome/fontawesome-free/svgs/solid/angle-double-up.svg";
|
||||
import fasArrowDown from "@fortawesome/fontawesome-free/svgs/solid/angle-double-down.svg";
|
||||
|
||||
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
|
||||
|
||||
import type { Item } from "./types";
|
||||
|
||||
import "./item-info";
|
||||
|
||||
// ===========================================================================
|
||||
class ItemIndex extends LitElement {
|
||||
@property({ type: Array })
|
||||
items: Item[] = [];
|
||||
|
||||
@property({ type: String })
|
||||
query = "";
|
||||
|
||||
@property({ type: Array })
|
||||
filteredItems: any[] = [];
|
||||
|
||||
@property({ type: Array })
|
||||
sortedItems: any[] | null = null;
|
||||
|
||||
@property({ type: Boolean })
|
||||
hideHeader: any = null;
|
||||
|
||||
@property({ type: String })
|
||||
dateName = "Date Loaded";
|
||||
|
||||
@property({ type: String })
|
||||
headerName = "Loaded Archives";
|
||||
|
||||
@state()
|
||||
private _deleting: any = {};
|
||||
|
||||
private typeFilter = "";
|
||||
private indexParams = "";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.hideHeader = localStorage.getItem("index:hideHeader") === "1";
|
||||
}
|
||||
|
||||
get sortKeys() {
|
||||
return [
|
||||
{ key: "title", name: "Title" },
|
||||
|
||||
{ key: "sourceUrl", name: "Source" },
|
||||
|
||||
{ key: "ctime", name: this.dateName },
|
||||
|
||||
{ key: "size", name: "Total Size" },
|
||||
];
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.loadItems();
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (changedProperties.has("hideHeader")) {
|
||||
localStorage.setItem("index:hideHeader", this.hideHeader ? "1" : "0");
|
||||
}
|
||||
if (changedProperties.has("colls") || changedProperties.has("query")) {
|
||||
this.filter();
|
||||
}
|
||||
}
|
||||
|
||||
filter() {
|
||||
if (!this.query) {
|
||||
this.filteredItems = this.items;
|
||||
return;
|
||||
}
|
||||
|
||||
this.filteredItems = [];
|
||||
|
||||
for (const item of this.items) {
|
||||
if (
|
||||
item.sourceUrl.indexOf(this.query) >= 0 ||
|
||||
item.filename.indexOf(this.query) >= 0 ||
|
||||
(item.loadUrl && item.loadUrl.indexOf(this.query) >= 0) ||
|
||||
(item.title && item.title.indexOf(this.query) >= 0)
|
||||
) {
|
||||
this.filteredItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadItems() {
|
||||
const resp = await fetch(`${apiPrefix}/coll-index?${this.indexParams}`);
|
||||
try {
|
||||
if (resp.status !== 200) {
|
||||
throw new Error("Invalid API Response, Retry");
|
||||
}
|
||||
const json = await resp.json();
|
||||
this.items = json.colls.map((item: Item) => {
|
||||
item.title = item.title || item.filename;
|
||||
return item;
|
||||
});
|
||||
|
||||
this._deleting = {};
|
||||
this.sortedItems = [];
|
||||
} catch (e) {
|
||||
// likely no sw registered yet, or waiting for new sw to register, retry again
|
||||
setTimeout(() => this.loadItems(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
async onDeleteItem(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!this.sortedItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(event.currentTarget.getAttribute("data-coll-index"));
|
||||
|
||||
const item = this.sortedItems[index];
|
||||
|
||||
if (!item || this._deleting[item.sourceUrl]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._deleting[item.sourceUrl] = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const resp = await fetch(`${apiPrefix}/c/${item.id}`, { method: "DELETE" });
|
||||
if (resp.status === 200) {
|
||||
const json = await resp.json();
|
||||
this.items = json.colls;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(ItemIndex.compStyles);
|
||||
}
|
||||
|
||||
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: 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;
|
||||
}
|
||||
|
||||
.panel-color {
|
||||
background-color: rgb(210, 249, 214);
|
||||
}
|
||||
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
return html`<h2 class="panel-heading panel-color">
|
||||
<span>${this.headerName}</span>
|
||||
</h2>`;
|
||||
}
|
||||
|
||||
renderSearchHeader() {
|
||||
return "";
|
||||
}
|
||||
|
||||
render() {
|
||||
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.items.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}"
|
||||
placeholder="Search by Archive Title or Source"
|
||||
/>
|
||||
<span class="icon is-left is-small">
|
||||
<fa-icon .svg="${fasSearch}"></fa-icon>
|
||||
</span>
|
||||
</div>
|
||||
<wr-sorter
|
||||
id="index"
|
||||
sortKey="ctime"
|
||||
?sortDesc="${true}"
|
||||
.sortKeys="${this.sortKeys}"
|
||||
.data="${this.filteredItems}"
|
||||
@sort-changed="${(e) =>
|
||||
(this.sortedItems = e.detail.sortedData)}"
|
||||
>
|
||||
</wr-sorter>
|
||||
</div>
|
||||
|
||||
<div class="coll-list">
|
||||
${this.sortedItems &&
|
||||
this.sortedItems.map(
|
||||
(item, i) => html`
|
||||
<div class="coll-block panel-block">
|
||||
${this.renderItemInfo(item)}
|
||||
${!this._deleting[item.sourceUrl]
|
||||
? html`
|
||||
<button
|
||||
class="delete delete-button"
|
||||
aria-label="Unload Item"
|
||||
title="Unload Item"
|
||||
data-coll-index="${i}"
|
||||
@click="${this.onDeleteItem}"
|
||||
></button>
|
||||
`
|
||||
: html` <span
|
||||
class="button delete-button is-loading is-static"
|
||||
>
|
||||
Deleting
|
||||
</span>`}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="panel-block extra-padding">
|
||||
${this.sortedItems === null
|
||||
? html`<i>Loading Archives...</i>`
|
||||
: this.renderEmpty()}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
renderItemInfo(item: Item) {
|
||||
return html`<wr-item-info .item=${item}></wr-item-info>`;
|
||||
}
|
||||
|
||||
renderEmpty() {
|
||||
return html`<i
|
||||
>No Archives so far! Archives loaded in the section above will appear
|
||||
here.</i
|
||||
>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-item-index", ItemIndex);
|
||||
|
||||
export { ItemIndex };
|
||||
@@ -0,0 +1,249 @@
|
||||
import { LitElement, html, css, nothing } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { wrapCss } from "./misc";
|
||||
import { map } from "lit/directives/map.js";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
|
||||
import type { Item } from "./types";
|
||||
|
||||
import "./components/labeled-field";
|
||||
|
||||
// ===========================================================================
|
||||
class ItemInfo extends LitElement {
|
||||
@property({ type: Object })
|
||||
item!: Item | Record<string, never>;
|
||||
|
||||
@property({ type: Boolean })
|
||||
detailed = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
canDelete = false;
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(ItemInfo.compStyles);
|
||||
}
|
||||
|
||||
static get compStyles() {
|
||||
return css`
|
||||
.columns {
|
||||
width: 100%;
|
||||
}
|
||||
.column {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
||||
:host(.is-list) .columns {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:host(.is-list) .column {
|
||||
width: 100% !important;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.col-title:hover {
|
||||
}
|
||||
.col-title a {
|
||||
display: block;
|
||||
height: 100%;
|
||||
}
|
||||
.column:hover > .copy,
|
||||
.col-content:hover .copy,
|
||||
.copy:hover {
|
||||
color: inherit;
|
||||
}
|
||||
.copy {
|
||||
color: black;
|
||||
margin: 0px;
|
||||
margin: -4px 0 0;
|
||||
line-height: 0.4em;
|
||||
padding: 6px;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
.copy:active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.col-content {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
}
|
||||
.minihead {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
renderSource(showItemID = true) {
|
||||
const item = this.item;
|
||||
return html`
|
||||
<wr-labeled-field
|
||||
label="Source"
|
||||
copy="${item.sourceUrl}"
|
||||
class="column is-4"
|
||||
>${item.sourceUrl &&
|
||||
(item.sourceUrl.startsWith("http://") ||
|
||||
item.sourceUrl.startsWith("https://"))
|
||||
? html` <a href="${item.sourceUrl}">${item.sourceUrl}</a> `
|
||||
: html` ${item.sourceUrl}`}
|
||||
${item.sourceUrl && item.sourceUrl.startsWith("googledrive://")
|
||||
? html` <i>(${item.filename})</i>`
|
||||
: nothing}
|
||||
</wr-labeled-field>
|
||||
${showItemID
|
||||
? html`<wr-labeled-field
|
||||
label="Archived Item ID"
|
||||
.copy=${item.coll}
|
||||
class="column"
|
||||
>
|
||||
${item.coll || "No ID"}
|
||||
</wr-labeled-field>`
|
||||
: nothing}
|
||||
<wr-labeled-field label="Date Loaded" class="column is-2">
|
||||
${item.ctime ? new Date(item.ctime).toLocaleString() : nothing}
|
||||
</wr-labeled-field>
|
||||
<wr-labeled-field label="Total Size" class="column is-2">
|
||||
${prettyBytes(Number(item.totalSize || item.size || 0))}
|
||||
</wr-labeled-field>
|
||||
`;
|
||||
}
|
||||
|
||||
renderSummaryView() {
|
||||
const item = this.item;
|
||||
|
||||
return html` <div class="columns">
|
||||
<div class="column col-title is-4">
|
||||
<span class="subtitle has-text-weight-bold">
|
||||
<a href="?source=${encodeURIComponent(item.sourceUrl)}"
|
||||
>${item.name || item.title || item.filename}</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
${this.renderSource(false)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
renderDetailed() {
|
||||
const item = this.item;
|
||||
|
||||
const {
|
||||
numValid = 0,
|
||||
numInvalid = 0,
|
||||
domain,
|
||||
certFingerprint,
|
||||
datapackageHash,
|
||||
publicKey,
|
||||
software,
|
||||
} = this.item.verify || {};
|
||||
|
||||
const certFingerprintUrl = certFingerprint
|
||||
? `https://crt.sh/?q=${certFingerprint}`
|
||||
: "";
|
||||
|
||||
return html` <div class="columns">
|
||||
${item.name || item.title
|
||||
? html`<wr-labeled-field label="Title" class="column">
|
||||
${item.name || item.title}
|
||||
</wr-labeled-field>`
|
||||
: nothing}
|
||||
<wr-labeled-field label="Filename" class="column">
|
||||
${item.filename}
|
||||
</wr-labeled-field>
|
||||
${item.resources
|
||||
? html`<wr-labeled-field label="Files" class="column">
|
||||
<ol style="padding: revert">
|
||||
${map(
|
||||
item.resources,
|
||||
(resource) =>
|
||||
html`<li>
|
||||
<a href="${resource.path}">${resource.name + "\n"}</a>
|
||||
</li>`,
|
||||
)}
|
||||
</ol>
|
||||
</wr-labeled-field>`
|
||||
: nothing}
|
||||
${this.renderSource()}
|
||||
${domain
|
||||
? html`
|
||||
<wr-labeled-field label="Observed By" class="column">
|
||||
<p>${domain}</p>
|
||||
${certFingerprintUrl
|
||||
? html`<span
|
||||
><a target="_blank" href="${certFingerprintUrl}"
|
||||
>View Certificate</a
|
||||
></span
|
||||
>`
|
||||
: nothing}
|
||||
</wr-labeled-field>
|
||||
`
|
||||
: nothing}
|
||||
${software
|
||||
? html`
|
||||
<wr-labeled-field label="Created With" class="column">
|
||||
${software || "Unknown"}
|
||||
</wr-labeled-field>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<wr-labeled-field label="Validation" class="column">
|
||||
${numValid > 0 || numInvalid > 0
|
||||
? html` <p>
|
||||
${numValid} hashes
|
||||
verified${numInvalid ? html`, ${numInvalid} invalid` : nothing}
|
||||
</p>`
|
||||
: html` Not Available`}
|
||||
</wr-labeled-field>
|
||||
|
||||
<wr-labeled-field
|
||||
label="Package Hash"
|
||||
class="column"
|
||||
.copy=${datapackageHash}
|
||||
>
|
||||
${datapackageHash || "Not Available"}
|
||||
</wr-labeled-field>
|
||||
|
||||
<wr-labeled-field
|
||||
label="Observer Public Key"
|
||||
class="column"
|
||||
.copy=${publicKey}
|
||||
>
|
||||
${publicKey || "Not Available"}
|
||||
</wr-labeled-field>
|
||||
|
||||
<wr-labeled-field label="Loading Mode" class="column">
|
||||
${item.onDemand ? "Download On-Demand" : "Fully Local"}
|
||||
</wr-labeled-field>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.detailed ? this.renderDetailed() : this.renderSummaryView();
|
||||
}
|
||||
|
||||
onCopy(event: Event, text: string | undefined) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (text) navigator.clipboard.writeText(text);
|
||||
return false;
|
||||
}
|
||||
|
||||
onPurge(reload) {
|
||||
const detail = { reload };
|
||||
this.dispatchEvent(new CustomEvent("item-purge", { detail }));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-item-info", ItemInfo);
|
||||
|
||||
export { ItemInfo };
|
||||
+71
-69
@@ -1,10 +1,9 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { ref, createRef, type Ref } from "lit/directives/ref.js";
|
||||
import type {
|
||||
SlDialog,
|
||||
SlDropdown,
|
||||
SlMenu,
|
||||
SlSelectEvent,
|
||||
} from "@shoelace-style/shoelace";
|
||||
import "@shoelace-style/shoelace/dist/components/alert/alert.js";
|
||||
@@ -52,13 +51,15 @@ import fasCaretDown from "@fortawesome/fontawesome-free/svgs/solid/caret-down.sv
|
||||
import { RWPEmbedReceipt } from "./embed-receipt.js";
|
||||
import Split from "split.js";
|
||||
|
||||
import type { Coll as CollInfo } from "./types";
|
||||
import type { Item as ItemInfo } from "./types";
|
||||
import type { Replay } from "./replay";
|
||||
|
||||
import "./item-info";
|
||||
|
||||
const RWP_SCHEME = "search://";
|
||||
|
||||
// ===========================================================================
|
||||
class Coll extends LitElement {
|
||||
class Item extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
inited = false;
|
||||
|
||||
@@ -72,10 +73,10 @@ class Coll extends LitElement {
|
||||
showSidebar: boolean | null = null;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
collInfo: CollInfo | Record<string, never> | null = null;
|
||||
itemInfo: ItemInfo | Record<string, never> | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
coll = "";
|
||||
item = "";
|
||||
|
||||
@property({ type: Boolean })
|
||||
hasStory = false;
|
||||
@@ -172,7 +173,7 @@ class Coll extends LitElement {
|
||||
|
||||
firstUpdated() {
|
||||
this.inited = true;
|
||||
window.addEventListener("hashchange", (event) => this.onHashChange());
|
||||
window.addEventListener("hashchange", () => this.onHashChange());
|
||||
|
||||
this.addEventListener("fullscreenchange", () => {
|
||||
this.isFullscreen = !!document.fullscreenElement;
|
||||
@@ -193,9 +194,9 @@ class Coll extends LitElement {
|
||||
while (
|
||||
this.editable &&
|
||||
this.autoUpdateInterval &&
|
||||
(!this.collInfo ||
|
||||
!this.collInfo.pages ||
|
||||
this.collInfo.pages.length < 100)
|
||||
(!this.itemInfo ||
|
||||
!this.itemInfo.pages ||
|
||||
this.itemInfo.pages.length < 100)
|
||||
) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, this.autoUpdateInterval * 1000),
|
||||
@@ -212,7 +213,7 @@ class Coll extends LitElement {
|
||||
const resp = await fetch(
|
||||
apiPrefix +
|
||||
"/c/" +
|
||||
this.coll +
|
||||
this.item +
|
||||
"/ts/?url=" +
|
||||
window.encodeURIComponent(this.tabData.url),
|
||||
);
|
||||
@@ -262,7 +263,7 @@ class Coll extends LitElement {
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("tabData")) {
|
||||
if (!this.collInfo || !this.collInfo.coll) {
|
||||
if (!this.itemInfo || !this.itemInfo.coll) {
|
||||
return;
|
||||
}
|
||||
const newHash = "#" + new URLSearchParams(this.tabData as any).toString();
|
||||
@@ -355,31 +356,31 @@ class Coll extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let coll = this.loadInfo && this.loadInfo.customColl;
|
||||
let item = this.loadInfo && this.loadInfo.customColl; // @todo(2023-11-06) update customColl -> customItem
|
||||
|
||||
if (!coll) {
|
||||
if (!item) {
|
||||
const res = await sourceToId(this.sourceUrl);
|
||||
coll = res.coll;
|
||||
item = res.coll;
|
||||
}
|
||||
|
||||
this.coll = coll;
|
||||
this.item = item;
|
||||
|
||||
const collApiPrefix = apiPrefix + "/c/" + coll;
|
||||
const collReplayPrefix = replayPrefix + "/" + coll;
|
||||
const itemApiPrefix = apiPrefix + "/c/" + item;
|
||||
const itemReplayPrefix = replayPrefix + "/" + item;
|
||||
|
||||
const resp = await fetch(collApiPrefix + "?all=1");
|
||||
const resp = await fetch(itemApiPrefix + "?all=1");
|
||||
|
||||
if (resp.status != 200) {
|
||||
this.collInfo = {};
|
||||
this.itemInfo = {};
|
||||
return;
|
||||
}
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
this.collInfo = {
|
||||
apiPrefix: collApiPrefix,
|
||||
replayPrefix: collReplayPrefix,
|
||||
coll,
|
||||
this.itemInfo = {
|
||||
apiPrefix: itemApiPrefix,
|
||||
replayPrefix: itemReplayPrefix,
|
||||
coll: item,
|
||||
...json,
|
||||
};
|
||||
|
||||
@@ -389,14 +390,14 @@ class Coll extends LitElement {
|
||||
this.loadInfo.extraConfig.headers
|
||||
) {
|
||||
const headers = this.loadInfo.extraConfig.headers;
|
||||
await fetch(`${collApiPrefix}/updateAuth`, {
|
||||
await fetch(`${itemApiPrefix}/updateAuth`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ headers }),
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.collInfo!.title) {
|
||||
this.collInfo!.title = this.collInfo!.filename;
|
||||
if (!this.itemInfo!.title) {
|
||||
this.itemInfo!.title = this.itemInfo!.filename;
|
||||
}
|
||||
|
||||
if (this.embed === "replayonly" || this.embed === "replay-with-info") {
|
||||
@@ -404,13 +405,13 @@ class Coll extends LitElement {
|
||||
}
|
||||
|
||||
this.hasStory = Boolean(
|
||||
this.collInfo!.desc || this.collInfo!.lists?.length,
|
||||
this.itemInfo!.desc || this.itemInfo!.lists?.length,
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("coll-loaded", {
|
||||
detail: {
|
||||
collInfo: this.collInfo,
|
||||
collInfo: this.itemInfo,
|
||||
alreadyLoaded: true,
|
||||
},
|
||||
}),
|
||||
@@ -419,7 +420,7 @@ class Coll extends LitElement {
|
||||
this.onHashChange();
|
||||
}
|
||||
|
||||
onCollLoaded(event) {
|
||||
onItemLoaded(event) {
|
||||
this.doUpdateInfo();
|
||||
this.loadInfo = null;
|
||||
if (event.detail.sourceUrl) {
|
||||
@@ -429,18 +430,18 @@ class Coll extends LitElement {
|
||||
new CustomEvent("coll-loaded", {
|
||||
detail: {
|
||||
sourceUrl: this.sourceUrl,
|
||||
collInfo: this.collInfo,
|
||||
collInfo: this.itemInfo,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
onCollUpdate(event) {
|
||||
onItemUpdate(event) {
|
||||
if (!this.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.collInfo = { ...this.collInfo, ...event.detail };
|
||||
this.itemInfo = { ...this.itemInfo, ...event.detail };
|
||||
}
|
||||
|
||||
onHashChange() {
|
||||
@@ -453,12 +454,12 @@ class Coll extends LitElement {
|
||||
}
|
||||
|
||||
if (
|
||||
this.collInfo?.coll &&
|
||||
this.itemInfo?.coll &&
|
||||
(!this.tabData.view || !this.tabNames.includes(this.tabData.view))
|
||||
) {
|
||||
const view = this.hasStory
|
||||
? "story"
|
||||
: this.editable || this.collInfo.pages?.length
|
||||
: this.editable || this.itemInfo.pages?.length
|
||||
? "pages"
|
||||
: "resources";
|
||||
|
||||
@@ -471,11 +472,11 @@ class Coll extends LitElement {
|
||||
if (this.tabData.url && this.tabData.url.startsWith("page:")) {
|
||||
const pageIndex = Number(this.tabData.url.slice("page:".length));
|
||||
if (
|
||||
this.collInfo?.pages &&
|
||||
this.itemInfo?.pages &&
|
||||
!isNaN(pageIndex) &&
|
||||
pageIndex < this.collInfo.pages.length
|
||||
pageIndex < this.itemInfo.pages.length
|
||||
) {
|
||||
const page = this.collInfo.pages[pageIndex];
|
||||
const page = this.itemInfo.pages[pageIndex];
|
||||
this.tabData.url = page.url;
|
||||
this.tabData.ts = getPageDateTS(page).timestamp;
|
||||
}
|
||||
@@ -498,7 +499,7 @@ class Coll extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
onCollTabNav(event) {
|
||||
onItemTabNav(event) {
|
||||
if (event.detail.reload) {
|
||||
this.onRefresh(null, true);
|
||||
return;
|
||||
@@ -533,7 +534,7 @@ class Coll extends LitElement {
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return wrapCss(Coll.compStyles);
|
||||
return wrapCss(Item.compStyles);
|
||||
}
|
||||
|
||||
static get compStyles() {
|
||||
@@ -626,7 +627,7 @@ class Coll extends LitElement {
|
||||
|
||||
${RWPEmbedReceipt.embedStyles}
|
||||
|
||||
${Coll.replayBarStyles}
|
||||
${Item.replayBarStyles}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -861,20 +862,20 @@ class Coll extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (this.collInfo && !this.collInfo.coll) {
|
||||
if (this.itemInfo && !this.itemInfo.coll) {
|
||||
return html` <wr-loader
|
||||
.loadInfo="${this.loadInfo}"
|
||||
embed="${this.embed || ""}"
|
||||
swName="${this.swName}"
|
||||
.coll="${this.coll}"
|
||||
.coll="${this.item}"
|
||||
sourceUrl="${this.sourceUrl || ""}"
|
||||
@coll-loaded=${this.onCollLoaded}
|
||||
@coll-loaded=${this.onItemLoaded}
|
||||
></wr-loader>`;
|
||||
} else if (this.collInfo) {
|
||||
} else if (this.itemInfo) {
|
||||
return html`
|
||||
${this.renderLocationBar()} ${this.renderVerifyInfo()}
|
||||
<sl-dialog label="Archive Info" ${ref(this.archiveInfoDialog)}>
|
||||
${this.renderCollInfo()}
|
||||
${this.renderItemInfo()}
|
||||
<sl-button
|
||||
slot="footer"
|
||||
variant="primary"
|
||||
@@ -894,7 +895,7 @@ class Coll extends LitElement {
|
||||
aria-label="${isSidebar ? "Browse Contents" : ""}"
|
||||
>
|
||||
${this.renderTabHeader(isSidebar)}
|
||||
${isSidebar || !isReplay ? this.renderCollTabs(isSidebar) : html``}
|
||||
${isSidebar || !isReplay ? this.renderItemTabs(isSidebar) : html``}
|
||||
</div>
|
||||
|
||||
${isReplay && this.isVisible
|
||||
@@ -902,11 +903,11 @@ class Coll extends LitElement {
|
||||
<wr-coll-replay
|
||||
role="main"
|
||||
tabindex="-1"
|
||||
.collInfo="${this.collInfo}"
|
||||
.collInfo="${this.itemInfo}"
|
||||
sourceUrl="${this.sourceUrl || ""}"
|
||||
url="${this.tabData.url || ""}"
|
||||
ts="${this.tabData.ts || ""}"
|
||||
@coll-tab-nav="${this.onCollTabNav}"
|
||||
@coll-tab-nav="${this.onItemTabNav}"
|
||||
id="replay"
|
||||
@replay-loading="${(e) =>
|
||||
(this.isLoading = e.detail.loading)}"
|
||||
@@ -1410,7 +1411,9 @@ class Coll extends LitElement {
|
||||
date,
|
||||
label: dateStr,
|
||||
});
|
||||
} catch {}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
});
|
||||
const currDateStr = tsToDate(this.ts).toLocaleString();
|
||||
return html`<div id="datetime" class="control is-hidden-mobile">
|
||||
@@ -1453,7 +1456,7 @@ class Coll extends LitElement {
|
||||
}
|
||||
|
||||
return html`<rwp-embed-receipt
|
||||
.collInfo=${this.collInfo || {}}
|
||||
.collInfo=${this.itemInfo || {}}
|
||||
url=${this.url}
|
||||
ts=${this.ts}
|
||||
.appLogo=${this.appLogo}
|
||||
@@ -1475,27 +1478,27 @@ class Coll extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
renderCollInfo() {
|
||||
if (!this.collInfo)
|
||||
renderItemInfo() {
|
||||
if (!this.itemInfo)
|
||||
return html`<sl-alert open variant="warning">
|
||||
<sl-icon slot="icon" name="exclamation-triangle"></sl-icon>
|
||||
<strong>Archive info is not available</strong><br />
|
||||
Please reload and try again.
|
||||
</sl-alert>`;
|
||||
return html`<wr-coll-info
|
||||
return html`<wr-item-info
|
||||
class="is-list"
|
||||
.coll="${this.collInfo}"
|
||||
.item="${this.itemInfo}"
|
||||
?detailed="${true}"
|
||||
?canDelete="${!this.embed}"
|
||||
@coll-purge="${this.onPurgeCache}"
|
||||
></wr-coll-info>`;
|
||||
@item-purge="${this.onPurgeCache}"
|
||||
></wr-item-info>`;
|
||||
}
|
||||
|
||||
renderExtraToolbar(/*isDropdown = false*/) {
|
||||
return "";
|
||||
}
|
||||
|
||||
renderCollTabs(isSidebar) {
|
||||
renderItemTabs(isSidebar) {
|
||||
const isStory = this.hasStory && this.tabData.view === "story";
|
||||
const isPages = this.tabData.view === "pages";
|
||||
const isResources = this.tabData.view === "resources";
|
||||
@@ -1503,10 +1506,10 @@ class Coll extends LitElement {
|
||||
return html`
|
||||
${isStory
|
||||
? html` <wr-coll-story
|
||||
.collInfo="${this.collInfo || {}}"
|
||||
.collInfo="${this.itemInfo || {}}"
|
||||
.active="${isStory}"
|
||||
currList="${this.tabData.currList || 0}"
|
||||
@coll-tab-nav="${this.onCollTabNav}"
|
||||
@coll-tab-nav="${this.onItemTabNav}"
|
||||
id="story"
|
||||
.isSidebar="${isSidebar}"
|
||||
class="${isStory ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
||||
@@ -1516,12 +1519,12 @@ class Coll extends LitElement {
|
||||
: ""}
|
||||
${isResources
|
||||
? html` <wr-coll-resources
|
||||
.collInfo="${this.collInfo || {}}"
|
||||
.collInfo="${this.itemInfo || {}}"
|
||||
.active="${isResources}"
|
||||
query="${this.tabData.query || ""}"
|
||||
urlSearchType="${this.tabData.urlSearchType || ""}"
|
||||
.currMime="${this.tabData.currMime || ""}"
|
||||
@coll-tab-nav="${this.onCollTabNav}"
|
||||
@coll-tab-nav="${this.onItemTabNav}"
|
||||
id="resources"
|
||||
.isSidebar="${isSidebar}"
|
||||
class="is-paddingless ${isResources ? "" : "is-hidden"} ${isSidebar
|
||||
@@ -1533,7 +1536,7 @@ class Coll extends LitElement {
|
||||
: ""}
|
||||
${isPages
|
||||
? html` <wr-page-view
|
||||
.collInfo="${this.collInfo}"
|
||||
.collInfo="${this.itemInfo}"
|
||||
.active="${isPages}"
|
||||
.editable="${this.editable}"
|
||||
.isSidebar="${isSidebar}"
|
||||
@@ -1541,9 +1544,9 @@ class Coll extends LitElement {
|
||||
query="${this.tabData.query || ""}"
|
||||
.url="${this.tabData.url || ""}"
|
||||
.ts="${this.tabData.ts || ""}"
|
||||
@coll-tab-nav="${this.onCollTabNav}"
|
||||
@coll-tab-nav="${this.onItemTabNav}"
|
||||
id="pages"
|
||||
@coll-update="${this.onCollUpdate}"
|
||||
@coll-update="${this.onItemUpdate}"
|
||||
class="${isPages ? "" : "is-hidden"} ${isSidebar ? "sidebar" : ""}"
|
||||
role="${(isSidebar ? "" : "main") as any}"
|
||||
>
|
||||
@@ -1655,7 +1658,7 @@ class Coll extends LitElement {
|
||||
}
|
||||
|
||||
async deleteFully(reload = false) {
|
||||
const deleteURL = this.collInfo!.apiPrefix + (reload ? "?reload=1" : "");
|
||||
const deleteURL = this.itemInfo!.apiPrefix + (reload ? "?reload=1" : "");
|
||||
|
||||
const resp = await fetch(deleteURL, {
|
||||
method: "DELETE",
|
||||
@@ -1704,7 +1707,6 @@ class Coll extends LitElement {
|
||||
}
|
||||
|
||||
onSelectTimestamp(event: SlSelectEvent) {
|
||||
const menu = event.currentTarget as SlMenu;
|
||||
const { item } = event.detail;
|
||||
this.updateTabData({ ts: item.value });
|
||||
}
|
||||
@@ -1801,6 +1803,6 @@ class Coll extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("wr-coll", Coll);
|
||||
customElements.define("wr-item", Item);
|
||||
|
||||
export { Coll };
|
||||
export { Item };
|
||||
+1
-1
@@ -231,5 +231,5 @@ export {
|
||||
unsafeCSS,
|
||||
unsafeSVG,
|
||||
apiPrefix,
|
||||
replayPrefix
|
||||
replayPrefix,
|
||||
};
|
||||
|
||||
+2
-2
@@ -320,8 +320,8 @@ class PageEntry extends LitElement {
|
||||
let inx = textContent.indexOf(this.query);
|
||||
|
||||
if (inx < 0) {
|
||||
let textLower = textContent.toLowerCase();
|
||||
let queryLower = query.toLowerCase();
|
||||
const textLower = textContent.toLowerCase();
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
inx = textLower.indexOf(queryLower);
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import { LitElement, html, css, unsafeCSS } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { property } from "lit/decorators.js";
|
||||
import { wrapCss, clickOnSpacebarPress } from "./misc";
|
||||
import ndjson from "fetch-ndjson";
|
||||
|
||||
@@ -61,7 +61,7 @@ class Pages extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
changeNeeded = false;
|
||||
|
||||
@property({ type: Set })
|
||||
@property({ attribute: false })
|
||||
selectedPages = new Set();
|
||||
|
||||
@property({ type: Boolean })
|
||||
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import { wrapCss, rwpLogo } from "./misc";
|
||||
import type { Coll } from "./types";
|
||||
import type { Item } from "./types";
|
||||
|
||||
// ===========================================================================
|
||||
class Replay extends LitElement {
|
||||
@property({ type: Object })
|
||||
collInfo: Coll | Record<string, never> | null = null;
|
||||
collInfo: Item | Record<string, never> | null = null;
|
||||
|
||||
@property({ type: String })
|
||||
sourceUrl: any = null;
|
||||
|
||||
+4
-1
@@ -3,8 +3,11 @@ 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";
|
||||
|
||||
// ===========================================================================
|
||||
/**
|
||||
* @typedef {{key: string;name: string;}[]} SortKeys
|
||||
* @prop {SortKeys} sortKeys
|
||||
*/
|
||||
class Sorter extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
+15
-3
@@ -1,8 +1,12 @@
|
||||
export interface Coll {
|
||||
export interface Item {
|
||||
filename: string;
|
||||
sourceUrl: string;
|
||||
replayPrefix: string;
|
||||
apiPrefix: string;
|
||||
|
||||
/**
|
||||
* Archived Item ID
|
||||
*/
|
||||
coll?: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
@@ -10,8 +14,16 @@ export interface Coll {
|
||||
desc?: string;
|
||||
description?: string;
|
||||
resources?: { path: string; name: string }[];
|
||||
verify?: any;
|
||||
onDemand?: any;
|
||||
verify?: {
|
||||
numValid?: number;
|
||||
numInvalid?: number;
|
||||
domain?: string;
|
||||
certFingerprint?: string;
|
||||
datapackageHash?: string;
|
||||
publicKey?: string;
|
||||
software?: string;
|
||||
};
|
||||
onDemand?: boolean;
|
||||
pages?: any[];
|
||||
lists?: any[];
|
||||
ctime?: string;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ReplayWebApp } from "./appmain";
|
||||
import { Chooser } from "./chooser";
|
||||
import { CollIndex, CollInfo } from "./coll-index";
|
||||
import { Coll } from "./coll";
|
||||
import { ItemIndex } from "./item-index";
|
||||
import { ItemInfo } from "./item-info";
|
||||
import { Item } from "./item";
|
||||
import { Story } from "./story";
|
||||
import { GDrive } from "./gdrive";
|
||||
import { Loader } from "./loader";
|
||||
@@ -16,9 +17,9 @@ import "./shoelace";
|
||||
export {
|
||||
ReplayWebApp,
|
||||
Chooser,
|
||||
CollIndex,
|
||||
CollInfo,
|
||||
Coll,
|
||||
ItemIndex as CollIndex, // @todo(2023-11-06) complete rename
|
||||
ItemInfo as CollInfo, // @todo(2023-11-06) complete rename
|
||||
Item as Coll, // @todo(2023-11-06) complete rename
|
||||
Story,
|
||||
GDrive,
|
||||
Loader,
|
||||
|
||||
@@ -8,6 +8,9 @@ import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
|
||||
import "keyword-mark-element/lib/keyword-mark.js";
|
||||
|
||||
// ===========================================================================
|
||||
/**
|
||||
* @prop {boolean | undefined} active
|
||||
*/
|
||||
class URLResources extends LitElement {
|
||||
static get filters() {
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user