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:
emma
2023-11-29 15:53:46 -05:00
parent e7b86626df
commit e9438eb724
35 changed files with 1352 additions and 966 deletions
+10 -7
View File
@@ -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
View File
@@ -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 },
};
-625
View File
@@ -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 };
+57
View File
@@ -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 };
+1
View File
@@ -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 });
+1
View File
@@ -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 =
+1 -1
View File
@@ -1,2 +1,2 @@
declare module "*.svg";
declare var __SW_NAME__: string;
declare let __SW_NAME__: string;
+359
View File
@@ -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 };
+249
View File
@@ -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
View File
@@ -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
View File
@@ -231,5 +231,5 @@ export {
unsafeCSS,
unsafeSVG,
apiPrefix,
replayPrefix
replayPrefix,
};
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+6 -5
View File
@@ -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,
+3
View File
@@ -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 [