Update NPM packaging, linting, and a bunch of types (#275)

Preparation for
https://github.com/webrecorder/archiveweb.page/issues/198 /
https://github.com/webrecorder/archiveweb.page/pull/199.

## Changes

- Organizes published files more cleanly, using `dist/types` folder as
the target for type declarations and sourcemaps
- Configures type field in `package.json`
- Updates ESLint & its config
  - Sets up type-informed linting rules mirroring browsertrix
- Adds a bunch of other useful eslint and eslint-typescript rules, and
applies auto-fixes to the codebase
- Cleans up a bunch of types throughout the codebase, particularly parts
of the code used by ArchiveWebPage
- Changes `ItemIndex#_deleting` from `private` to `protected` so that a
part of AWP that extends it can do so properly
- Makes a bunch of previously-implied [non-null
assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-)
explicit
- Updates `URLResource` and `ItemType` types (and renames `Item` →
`ItemType` to disambiguate from the `Item` class)
- Removes `node-sass` and replaces it with `sass-embedded` for much
quicker installs and compilation
This commit is contained in:
Emma Segal-Grossman
2024-02-06 18:50:12 -08:00
committed by GitHub
parent a40471128f
commit a63390f10c
32 changed files with 898 additions and 1550 deletions
+92 -14
View File
@@ -1,32 +1,110 @@
/* eslint-env node */ /* eslint-env node */
/** @type { import("eslint").Linter.Config } */ /** @type {import('eslint').Linter.Config} */
module.exports = { module.exports = {
parser: "@typescript-eslint/parser",
env: { env: {
browser: true, browser: true,
es6: true, commonjs: true,
es2017: true,
}, },
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:wc/recommended",
"plugin:lit/recommended", "plugin:lit/recommended",
"prettier", "prettier",
"plugin:@typescript-eslint/recommended",
], ],
plugins: ["@typescript-eslint", "lit"], plugins: ["@typescript-eslint", "lit"],
parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
ecmaVersion: 2018, project: ["./tsconfig.eslint.json"],
sourceType: "module", tsconfigRootDir: __dirname,
}, },
root: true,
rules: { rules: {
/* start stylistic rules */
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/consistent-type-imports": [
"error",
{
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/prefer-readonly": "warn",
"@typescript-eslint/class-literal-property-style": ["warn", "getters"],
"@typescript-eslint/consistent-generic-constructors": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/no-confusing-non-null-assertion": "warn",
"@typescript-eslint/no-inferrable-types": "warn",
"@typescript-eslint/non-nullable-type-assertion-style": "warn",
"@typescript-eslint/prefer-for-of": "warn",
// "@typescript-eslint/prefer-nullish-coalescing": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
/* end stylistic rules */
/* start recommended rules */
"no-restricted-globals": [2, "event", "error"], "no-restricted-globals": [2, "event", "error"],
"linebreak-style": ["error", "unix"], "@typescript-eslint/no-base-to-string": "warn",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-duplicate-type-constituents": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-extra-non-null-assertion": "error",
// "@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-for-in-array": "warn",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
},
],
"no-implied-eval": "off",
"@typescript-eslint/no-implied-eval": "error",
"no-loss-of-precision": "off",
"@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-misused-promises": [
"error",
{ checksVoidReturn: { arguments: false } },
],
"@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "warn",
"@typescript-eslint/no-redundant-type-constituents": "warn",
"@typescript-eslint/no-this-alias": "warn",
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
// "@typescript-eslint/no-unsafe-argument": "warn",
// "@typescript-eslint/no-unsafe-assignment": "warn",
// "@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-declaration-merging": "warn",
"@typescript-eslint/no-unsafe-enum-comparison": "warn",
// "@typescript-eslint/no-unsafe-member-access": "warn",
// "@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/prefer-as-const": "warn",
"require-await": "off",
"@typescript-eslint/require-await": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/unbound-method": "off",
}, },
ignorePatterns: [
"ruffle/**/*",
"build/**/*",
"/sw.js",
"/ui.js",
"dist/**/*",
],
reportUnusedDisableDirectives: true, reportUnusedDisableDirectives: true,
ignorePatterns: [
"__generated__",
"__mocks__",
"dist",
"ruffle",
"ui.js",
"sw.js",
],
overrides: [
{
extends: ["plugin:@typescript-eslint/disable-type-checked"],
files: ["webpack.*.js"],
rules: {
"@typescript-eslint/no-var-requires": "off",
},
},
],
}; };
+1
View File
@@ -1,3 +1,4 @@
/*eslint-env node */
const { notarize } = require('electron-notarize'); const { notarize } = require('electron-notarize');
exports.default = async function notarizing(context) { exports.default = async function notarizing(context) {
-1
View File
@@ -1 +0,0 @@
export * from "./ui.js";
+11 -9
View File
@@ -7,6 +7,8 @@
"homepage": "https://replayweb.page/", "homepage": "https://replayweb.page/",
"author": "Webrecorder Software", "author": "Webrecorder Software",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"main": "ui.js",
"types": "dist/types/index.d.ts",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4", "@fortawesome/fontawesome-free": "^5.15.4",
"@shoelace-style/shoelace": "^2.8.0", "@shoelace-style/shoelace": "^2.8.0",
@@ -29,26 +31,27 @@
"stream-browserify": "^3.0.0" "stream-browserify": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/flexsearch": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@playwright/test": "^1.38.1", "@playwright/test": "^1.38.1",
"@types/flexsearch": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"copy-webpack-plugin": "^9.0.1", "copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0", "css-loader": "^6.2.0",
"electron": "^25.1.1", "electron": "^25.1.1",
"electron-builder": "^23.0.3", "electron-builder": "^23.0.3",
"electron-notarize": "^1.1.1", "electron-notarize": "^1.1.1",
"eslint": "^8.52.0", "eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-lit": "^1.9.1", "eslint-plugin-lit": "^1.10.1",
"eslint-plugin-wc": "^1.3.2",
"http-server": "^13.0.2", "http-server": "^13.0.2",
"husky": "^8.0.0", "husky": "^8.0.0",
"lint-staged": "^14.0.0", "lint-staged": "^14.0.0",
"mini-css-extract-plugin": "^2.3.0", "mini-css-extract-plugin": "^2.3.0",
"node-sass": "^9.0.0",
"playwright": "^1.38.1", "playwright": "^1.38.1",
"prettier": "^3.0.1", "prettier": "^3.0.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"sass-embedded": "^1.70.0",
"sass-loader": "^13.3.2", "sass-loader": "^13.3.2",
"style-loader": "^3.2.1", "style-loader": "^3.2.1",
"svg-inline-loader": "^0.8.2", "svg-inline-loader": "^0.8.2",
@@ -62,11 +65,11 @@
}, },
"files": [ "files": [
"/assets", "/assets",
"/dist",
"/src", "/src",
"/ruffle", "/ruffle",
"sw.js", "sw.js",
"ui.js", "ui.js",
"index.js",
"index.html" "index.html"
], ],
"scripts": { "scripts": {
@@ -102,7 +105,6 @@
"dist/*.js", "dist/*.js",
"dist/prebuilds/${platform}-${arch}/*", "dist/prebuilds/${platform}-${arch}/*",
"dist/build/*", "dist/build/*",
"index.js",
"ui.js", "ui.js",
"sw.js", "sw.js",
"*.html", "*.html",
+1 -2
View File
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */ /* eslint-env node */
// Runs a webpack build when not installed in a git repository (i.e. when installed from GitHub in node_modules) // Runs a webpack build when not installed in a git repository (i.e. when installed from GitHub in node_modules)
@@ -26,7 +25,7 @@ const git = (args) => cp.spawnSync("git", args, { stdio: "inherit" });
// Ensure that we're inside a Git repository // Ensure that we're inside a Git repository
// If git command is not found, status is null and we should return // If git command is not found, status is null and we should return
// That's why status value needs to be checked explicitly // That's why status value needs to be checked explicitly
if (git(["rev-parse"]).status !== 0) { if (git(["rev-parse"]).status !== 0 || process.argv.includes("--force")) {
l(`git command not found, running build`); l(`git command not found, running build`);
const compiler = webpack(config.map((c) => c())); const compiler = webpack(config.map((c) => c()));
+11 -14
View File
@@ -2,6 +2,7 @@ import {
LitElement, LitElement,
html, html,
css, css,
type CSSResultGroup,
type TemplateResult, type TemplateResult,
type PropertyValues, type PropertyValues,
} from "lit"; } from "lit";
@@ -22,7 +23,7 @@ import { SWManager } from "./swmanager";
import "./item"; import "./item";
import "./item-index"; import "./item-index";
import "./chooser"; import "./chooser";
import { LoadInfo } from "./item"; import { type LoadInfo } from "./item";
// =========================================================================== // ===========================================================================
@customElement("replay-app-main") @customElement("replay-app-main")
@@ -69,10 +70,10 @@ export class ReplayWebApp extends LitElement {
@property({ type: Boolean }) @property({ type: Boolean })
skipRuffle = false; skipRuffle = false;
@property({ type: Object }) @property({ type: String })
swErrorMsg: TemplateResult<1> | "" | null = null; swErrorMsg: TemplateResult<1> | string | null = null;
private swName?: string; protected swName?: string;
private swmanager: SWManager | null; private swmanager: SWManager | null;
private useRuffle = false; private useRuffle = false;
@@ -112,7 +113,7 @@ export class ReplayWebApp extends LitElement {
}); });
} }
private maybeStartFileDrop = (dragEvent: DragEvent) => { private readonly maybeStartFileDrop = (dragEvent: DragEvent) => {
if (this.sourceUrl) { if (this.sourceUrl) {
// A source is already loaded. Don't allow dropping a file. // A source is already loaded. Don't allow dropping a file.
return; return;
@@ -133,7 +134,7 @@ export class ReplayWebApp extends LitElement {
return wrapCss(ReplayWebApp.appStyles); return wrapCss(ReplayWebApp.appStyles);
} }
static get appStyles() { static get appStyles(): CSSResultGroup {
return css` return css`
#wrlogo { #wrlogo {
max-height: 1rem; max-height: 1rem;
@@ -237,7 +238,7 @@ export class ReplayWebApp extends LitElement {
} }
get mainLogo() { get mainLogo() {
return rwpLogo; return rwpLogo as string;
} }
renderNavBrand() { renderNavBrand() {
@@ -499,7 +500,7 @@ export class ReplayWebApp extends LitElement {
// This is a workaround, since this app's routing doesn't permit normal // This is a workaround, since this app's routing doesn't permit normal
// following of in-page anchors. // following of in-page anchors.
event.preventDefault(); event.preventDefault();
(this.renderRoot.querySelector("#skip-main-target") as HTMLElement).focus(); this.renderRoot.querySelector<HTMLElement>("#skip-main-target")?.focus();
} }
onNavMenu(event) { onNavMenu(event) {
@@ -516,9 +517,7 @@ export class ReplayWebApp extends LitElement {
(event) => { (event) => {
event.preventDefault(); event.preventDefault();
this.navMenuShown = false; this.navMenuShown = false;
( this.renderRoot.querySelector<HTMLElement>("#menu-button")?.focus();
this.renderRoot.querySelector("#menu-button") as HTMLElement
).focus();
}, },
{ once: true }, { once: true },
); );
@@ -528,9 +527,7 @@ export class ReplayWebApp extends LitElement {
if (event.key == "Escape") { if (event.key == "Escape") {
event.preventDefault(); event.preventDefault();
this.navMenuShown = false; this.navMenuShown = false;
( this.renderRoot.querySelector<HTMLElement>("#menu-button")?.focus();
this.renderRoot.querySelector("#menu-button") as HTMLElement
).focus();
} }
}, },
{ once: true }, { once: true },
+10 -22
View File
@@ -1,8 +1,8 @@
/*eslint-env node */ /*eslint-env node */
import { CollectionLoader } from "@webrecorder/wabac/src/loaders"; import { CollectionLoader } from "@webrecorder/wabac/src/loaders";
import { type IpcRendererEvent } from "electron";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { ipcRenderer, contextBridge } = require("electron"); const { ipcRenderer, contextBridge } = require("electron");
contextBridge.exposeInMainWorld("electron", { IS_APP: true }); contextBridge.exposeInMainWorld("electron", { IS_APP: true });
@@ -11,12 +11,7 @@ const dbs = {};
const loader = new CollectionLoader(); const loader = new CollectionLoader();
/** async function getColl(name: string) {
*
* @param {string} name
* @returns
*/
async function getColl(name) {
if (!dbs[name]) { if (!dbs[name]) {
dbs[name] = await loader.loadColl(name); dbs[name] = await loader.loadColl(name);
await dbs[name].initing; await dbs[name].initing;
@@ -25,25 +20,18 @@ async function getColl(name) {
return dbs[name]; return dbs[name];
} }
/** async function getDB(name: string) {
* @param {string} name
* @returns
*/
async function getDB(name) {
const coll = await getColl(name); const coll = await getColl(name);
return coll.store; return coll.store;
} }
/** async function getResponse(
* event: IpcRendererEvent,
* @param {import("electron").IpcRendererEvent} event request: Request,
* @param {*} request coll: string,
* @param {*} coll ts: string,
* @param {*} ts channel: string,
* @param {*} channel ): Promise<void> {
* @returns
*/
async function getResponse(event, request, coll, ts, channel) {
const db = await getDB(coll); const db = await getDB(coll);
const req = { request, url: request.url, timestamp: ts }; const req = { request, url: request.url, timestamp: ts };
+9 -3
View File
@@ -250,7 +250,14 @@ class ElectronReplayApp {
return rv; return rv;
} }
async doIntercept(request, callback) { async doIntercept(
request,
callback: (response: {
statusCode: number;
headers: Record<string, string>;
data: fs.ReadStream;
}) => void,
) {
console.log(`${request.method} ${request.url} from ${request.referrer}`); console.log(`${request.method} ${request.url} from ${request.referrer}`);
// if local server // if local server
@@ -303,7 +310,7 @@ class ElectronReplayApp {
} }
// possible 'live leak' attempt, return archived version, if any // possible 'live leak' attempt, return archived version, if any
if (request.referrer && request.referrer.startsWith(REPLAY_PREFIX)) { if (request.referrer?.startsWith(REPLAY_PREFIX)) {
return await this.resolveArchiveResponse(request, callback); return await this.resolveArchiveResponse(request, callback);
} }
@@ -483,7 +490,6 @@ class ElectronReplayApp {
} }
getOpenUrl(argv) { getOpenUrl(argv) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
argv = require("minimist")(argv.slice(process.defaultApp ? 2 : 1)); argv = require("minimist")(argv.slice(process.defaultApp ? 2 : 1));
const filename = const filename =
+2 -2
View File
@@ -10,7 +10,7 @@ import { LitElement, html, css, nothing } from "lit";
import { tsToDate } from "./pageutils"; import { tsToDate } from "./pageutils";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import type { Item as ItemType } from "./types"; import type { ItemType } from "./types";
// =========================================================================== // ===========================================================================
export class RWPEmbedReceipt extends LitElement { export class RWPEmbedReceipt extends LitElement {
@@ -183,7 +183,7 @@ export class RWPEmbedReceipt extends LitElement {
datapackageHash, datapackageHash,
publicKey, publicKey,
software, software,
} = this.collInfo?.verify || {}; } = this.collInfo?.verify ?? {};
const sourceUrl = this.collInfo?.sourceUrl; const sourceUrl = this.collInfo?.sourceUrl;
+2 -3
View File
@@ -2,7 +2,7 @@ import {
LitElement, LitElement,
html, html,
css, css,
TemplateResult, type TemplateResult,
type PropertyValues, type PropertyValues,
} from "lit"; } from "lit";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
@@ -53,7 +53,6 @@ class Embed extends LitElement {
@property({ type: String }) newWindowBase = ""; @property({ type: String }) newWindowBase = "";
@property({ type: String }) errorMessage: @property({ type: String }) errorMessage:
| ""
| TemplateResult<1> | TemplateResult<1>
| string | string
| undefined; | undefined;
@@ -163,7 +162,7 @@ class Embed extends LitElement {
} }
// if no storage manager or estimate, don't cache // if no storage manager or estimate, don't cache
if (!navigator.storage || !navigator.storage.estimate) { if (!navigator.storage?.estimate) {
this.noCache = true; this.noCache = true;
} }
} }
+46
View File
@@ -0,0 +1,46 @@
declare module "*.svg";
declare module "*.html";
declare module "*.scss";
declare module "*.sass";
declare const __SW_NAME__: string;
declare const __HELPER_PROXY__: string;
declare const __GDRIVE_CLIENT_ID__: string;
declare const __VERSION__: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TODOFixMe = any;
// showOpenFilePicker doesn't yet exist in Typescript's DOM lib
// see https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker for details
interface Window {
showOpenFilePicker: (options?: {
types: { description: string; accept: Record<string, string[]> }[];
}) => Promise<[FileSystemFileHandle]>;
}
// This is only supported in Blink (see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission#browser_compatibility), so Typescript leaves it out of their definitions (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1393#issuecomment-1483735315)
interface FileSystemHandle {
requestPermission: (options?: {
mode?: "read" | "readwrite";
}) => Promise<PermissionStatus>;
}
type GAPIResponse =
| { error: string; access_token: never }
| { error: never; access_token: string };
interface Window {
gapi: {
load: (method: string, callback: (response: GAPIResponse) => void) => void;
auth2: {
authorize: (
config: {
client_id: string;
scope: string;
response_type: string;
prompt: unknown;
},
callback: (response: GAPIResponse) => void,
) => void;
};
};
}
-21
View File
@@ -1,21 +0,0 @@
declare module "*.svg";
declare module "*.html";
declare const __SW_NAME__: string;
declare const __HELPER_PROXY__: string;
declare const __GDRIVE_CLIENT_ID__: string;
declare const __VERSION__: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type TODOFixMe = any;
// showOpenFilePicker doesn't yet exist in Typescript's DOM lib
// see https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker for details
interface Window {
showOpenFilePicker: (options?: {
types: { description: string; accept: Record<string, string[]> }[];
}) => Promise<[FileSystemFileHandle]>;
}
interface Window {
gapi: TODOFixMe;
}
+17 -3
View File
@@ -17,9 +17,21 @@ import "./shoelace";
export { export {
ReplayWebApp, ReplayWebApp,
Chooser, Chooser,
ItemIndex as CollIndex, // @todo(2023-11-06) complete rename ItemIndex,
ItemInfo as CollInfo, // @todo(2023-11-06) complete rename ItemInfo,
Item as Coll, // @todo(2023-11-06) complete rename Item,
/**
* @deprecated use {@linkcode ItemIndex}
*/
ItemIndex as CollIndex,
/**
* @deprecated use {@linkcode ItemInfo}
*/
ItemInfo as CollInfo,
/**
* @deprecated use {@linkcode Item}
*/
Item as Coll,
Story, Story,
GDrive, GDrive,
Loader, Loader,
@@ -30,3 +42,5 @@ export {
URLResources, URLResources,
Embed, Embed,
}; };
export type { ItemType, URLResource } from "./types";
+28 -17
View File
@@ -7,26 +7,26 @@ import fasArrowDown from "@fortawesome/fontawesome-free/svgs/solid/angle-double-
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg"; import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import type { Item } from "./types"; import type { ItemType } from "./types";
import "./item-info"; import "./item-info";
// =========================================================================== // ===========================================================================
class ItemIndex extends LitElement { class ItemIndex extends LitElement {
@property({ type: Array }) @property({ type: Array })
items: Item[] = []; items: ItemType[] = [];
@property({ type: String }) @property({ type: String })
query = ""; query = "";
@property({ type: Array }) @property({ type: Array })
filteredItems: Item[] = []; filteredItems: ItemType[] = [];
@property({ type: Array }) @property({ type: Array })
sortedItems: Item[] = []; sortedItems: ItemType[] = [];
@property({ type: Boolean }) @property({ type: Boolean })
hideHeader: boolean = false; hideHeader = false;
@property({ type: String }) @property({ type: String })
dateName = "Date Loaded"; dateName = "Date Loaded";
@@ -36,10 +36,14 @@ class ItemIndex extends LitElement {
@state() @state()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme
private _deleting: any = {}; protected _deleting: any = {};
private typeFilter = ""; private get typeFilter() {
private indexParams = ""; return "";
}
private get indexParams() {
return "";
}
constructor() { constructor() {
super(); super();
@@ -84,7 +88,7 @@ class ItemIndex extends LitElement {
if ( if (
item.sourceUrl.indexOf(this.query) >= 0 || item.sourceUrl.indexOf(this.query) >= 0 ||
item.filename.indexOf(this.query) >= 0 || item.filename.indexOf(this.query) >= 0 ||
(item.loadUrl && item.loadUrl.indexOf(this.query) >= 0) || Boolean(item.loadUrl && item.loadUrl.indexOf(this.query) >= 0) ||
(item.title && item.title.indexOf(this.query) >= 0) (item.title && item.title.indexOf(this.query) >= 0)
) { ) {
this.filteredItems.push(item); this.filteredItems.push(item);
@@ -99,8 +103,8 @@ class ItemIndex extends LitElement {
throw new Error("Invalid API Response, Retry"); throw new Error("Invalid API Response, Retry");
} }
const json = await resp.json(); const json = await resp.json();
this.items = json.colls.map((item: Item) => { this.items = json.colls.map((item: ItemType) => {
item.title = item.title || item.filename; item.title = item.title ?? item.filename;
return item; return item;
}); });
@@ -285,7 +289,10 @@ class ItemIndex extends LitElement {
<input <input
type="text" type="text"
class="input is-small" class="input is-small"
@input="${(e) => (this.query = e.currentTarget.value)}" @input="${(e: Event) =>
(this.query = (
e.currentTarget as HTMLInputElement
).value)}"
.value="${this.query}" .value="${this.query}"
placeholder="Search by Archive Title or Source" placeholder="Search by Archive Title or Source"
/> />
@@ -299,15 +306,19 @@ class ItemIndex extends LitElement {
?sortDesc="${true}" ?sortDesc="${true}"
.sortKeys="${this.sortKeys}" .sortKeys="${this.sortKeys}"
.data="${this.filteredItems}" .data="${this.filteredItems}"
@sort-changed="${(e) => @sort-changed="${(
(this.sortedItems = e.detail.sortedData)}" e: CustomEvent<{
sortKey: string | null;
sortDesc: boolean | null;
sortedData: ItemType[];
}>,
) => (this.sortedItems = e.detail.sortedData)}"
> >
</wr-sorter> </wr-sorter>
</div> </div>
<div class="coll-list"> <div class="coll-list">
${this.sortedItems && ${this.sortedItems?.map(
this.sortedItems.map(
(item, i) => html` (item, i) => html`
<div class="coll-block panel-block"> <div class="coll-block panel-block">
${this.renderItemInfo(item)} ${this.renderItemInfo(item)}
@@ -343,7 +354,7 @@ class ItemIndex extends LitElement {
`; `;
} }
renderItemInfo(item: Item) { renderItemInfo(item: ItemType) {
return html`<wr-item-info .item=${item}></wr-item-info>`; return html`<wr-item-info .item=${item}></wr-item-info>`;
} }
+23 -23
View File
@@ -4,14 +4,14 @@ import { wrapCss } from "./misc";
import { map } from "lit/directives/map.js"; import { map } from "lit/directives/map.js";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import type { Item } from "./types"; import type { ItemType } from "./types";
import "./components/labeled-field"; import "./components/labeled-field";
// =========================================================================== // ===========================================================================
class ItemInfo extends LitElement { class ItemInfo extends LitElement {
@property({ type: Object }) @property({ type: Object })
item!: Item | Record<string, never>; item: ItemType | Record<string, never> | null = null;
@property({ type: Boolean }) @property({ type: Boolean })
detailed = false; detailed = false;
@@ -90,31 +90,31 @@ class ItemInfo extends LitElement {
return html` return html`
<wr-labeled-field <wr-labeled-field
label="Source" label="Source"
copy="${item.sourceUrl}" copy="${item!.sourceUrl}"
class="column is-4" class="column is-4"
>${item.sourceUrl && >${item!.sourceUrl &&
(item.sourceUrl.startsWith("http://") || (item!.sourceUrl.startsWith("http://") ||
item.sourceUrl.startsWith("https://")) item!.sourceUrl.startsWith("https://"))
? html` <a href="${item.sourceUrl}">${item.sourceUrl}</a> ` ? html` <a href="${item!.sourceUrl}">${item!.sourceUrl}</a> `
: html` ${item.sourceUrl}`} : html` ${item!.sourceUrl}`}
${item.sourceUrl && item.sourceUrl.startsWith("googledrive://") ${item!.sourceUrl && item!.sourceUrl.startsWith("googledrive://")
? html` <i>(${item.filename})</i>` ? html` <i>(${item!.filename})</i>`
: nothing} : nothing}
</wr-labeled-field> </wr-labeled-field>
${showItemID ${showItemID
? html`<wr-labeled-field ? html`<wr-labeled-field
label="Archived Item ID" label="Archived Item ID"
.copy=${item.coll} .copy=${item!.coll}
class="column" class="column"
> >
${item.coll || "No ID"} ${item!.coll || "No ID"}
</wr-labeled-field>` </wr-labeled-field>`
: nothing} : nothing}
<wr-labeled-field label="Date Loaded" class="column is-2"> <wr-labeled-field label="Date Loaded" class="column is-2">
${item.ctime ? new Date(item.ctime).toLocaleString() : nothing} ${item!.ctime ? new Date(item!.ctime).toLocaleString() : nothing}
</wr-labeled-field> </wr-labeled-field>
<wr-labeled-field label="Total Size" class="column is-2"> <wr-labeled-field label="Total Size" class="column is-2">
${prettyBytes(Number(item.totalSize || item.size || 0))} ${prettyBytes(Number(item!.totalSize || item!.size || 0))}
</wr-labeled-field> </wr-labeled-field>
`; `;
} }
@@ -125,8 +125,8 @@ class ItemInfo extends LitElement {
return html` <div class="columns"> return html` <div class="columns">
<div class="column col-title is-4"> <div class="column col-title is-4">
<span class="subtitle has-text-weight-bold"> <span class="subtitle has-text-weight-bold">
<a href="?source=${encodeURIComponent(item.sourceUrl)}" <a href="?source=${encodeURIComponent(item!.sourceUrl)}"
>${item.name || item.title || item.filename}</a >${item!.name || item!.title || item!.filename}</a
> >
</span> </span>
</div> </div>
@@ -145,26 +145,26 @@ class ItemInfo extends LitElement {
datapackageHash, datapackageHash,
publicKey, publicKey,
software, software,
} = this.item.verify || {}; } = this.item!.verify || {};
const certFingerprintUrl = certFingerprint const certFingerprintUrl = certFingerprint
? `https://crt.sh/?q=${certFingerprint}` ? `https://crt.sh/?q=${certFingerprint}`
: ""; : "";
return html` <div class="columns"> return html` <div class="columns">
${item.name || item.title ${item!.name || item!.title
? html`<wr-labeled-field label="Title" class="column"> ? html`<wr-labeled-field label="Title" class="column">
${item.name || item.title} ${item!.name || item!.title}
</wr-labeled-field>` </wr-labeled-field>`
: nothing} : nothing}
<wr-labeled-field label="Filename" class="column"> <wr-labeled-field label="Filename" class="column">
${item.filename} ${item!.filename}
</wr-labeled-field> </wr-labeled-field>
${item.resources ${item!.resources
? html`<wr-labeled-field label="Files" class="column"> ? html`<wr-labeled-field label="Files" class="column">
<ol style="padding: revert"> <ol style="padding: revert">
${map( ${map(
item.resources, item!.resources,
(resource) => (resource) =>
html`<li> html`<li>
<a href="${resource.path}">${resource.name + "\n"}</a> <a href="${resource.path}">${resource.name + "\n"}</a>
@@ -222,7 +222,7 @@ class ItemInfo extends LitElement {
</wr-labeled-field> </wr-labeled-field>
<wr-labeled-field label="Loading Mode" class="column"> <wr-labeled-field label="Loading Mode" class="column">
${item.onDemand ? "Download On-Demand" : "Fully Local"} ${item!.onDemand ? "Download On-Demand" : "Fully Local"}
</wr-labeled-field> </wr-labeled-field>
</div>`; </div>`;
} }
+34 -43
View File
@@ -1,4 +1,10 @@
import { LitElement, html, css, type PropertyValues } from "lit"; import {
LitElement,
html,
css,
type PropertyValues,
type TemplateResult,
} from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import { ref, createRef, type Ref } from "lit/directives/ref.js"; import { ref, createRef, type Ref } from "lit/directives/ref.js";
import type { import type {
@@ -51,7 +57,7 @@ import fasCaretDown from "@fortawesome/fontawesome-free/svgs/solid/caret-down.sv
import { RWPEmbedReceipt } from "./embed-receipt"; import { RWPEmbedReceipt } from "./embed-receipt";
import Split from "split.js"; import Split from "split.js";
import type { Item as ItemType } from "./types"; import type { ItemType, URLResource } from "./types";
import type { Replay } from "./replay"; import type { Replay } from "./replay";
import { ifDefined } from "lit/directives/if-defined.js"; import { ifDefined } from "lit/directives/if-defined.js";
@@ -76,6 +82,7 @@ export type LoadInfo = {
swError?: string; swError?: string;
newFullImport?: unknown; newFullImport?: unknown;
name?: string; name?: string;
importCollId?: string;
}; };
// =========================================================================== // ===========================================================================
@@ -175,14 +182,13 @@ class Item extends LitElement {
private observer?: IntersectionObserver; private observer?: IntersectionObserver;
private archiveInfoDialog: Ref<SlDialog> = createRef(); private readonly archiveInfoDialog: Ref<SlDialog> = createRef();
private readonly tabNames = ["pages", "story", "resources", "info"]; private readonly tabNames = ["pages", "story", "resources", "info"];
private readonly tabLabels = { private readonly tabLabels = {
pages: "Pages", pages: "Pages",
story: "Story", story: "Story",
resources: "URLs", resources: "URLs",
info: "Archive Info",
}; };
constructor() { constructor() {
@@ -214,9 +220,7 @@ class Item extends LitElement {
while ( while (
this.editable && this.editable &&
this.autoUpdateInterval && this.autoUpdateInterval &&
(!this.itemInfo || (!this.itemInfo?.pages || this.itemInfo.pages.length < 100)
!this.itemInfo.pages ||
this.itemInfo.pages.length < 100)
) { ) {
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, this.autoUpdateInterval * 1000), setTimeout(resolve, this.autoUpdateInterval * 1000),
@@ -283,7 +287,7 @@ class Item extends LitElement {
} }
} }
if (changedProperties.has("tabData")) { if (changedProperties.has("tabData")) {
if (!this.itemInfo || !this.itemInfo.coll) { if (!this.itemInfo?.coll) {
return; return;
} }
const newHash = const newHash =
@@ -308,9 +312,8 @@ class Item extends LitElement {
} else { } else {
window.location.hash = this._locationHash; window.location.hash = this._locationHash;
if (!this.showSidebar) { if (!this.showSidebar) {
const replay = this.renderRoot.querySelector( const replay =
"wr-coll-replay", this.renderRoot.querySelector<Replay>("wr-coll-replay");
) as Replay;
if (replay) { if (replay) {
replay.focus(); replay.focus();
} }
@@ -338,10 +341,8 @@ class Item extends LitElement {
configureSplitter() { configureSplitter() {
if (this.tabData.url && this.showSidebar) { if (this.tabData.url && this.showSidebar) {
const contents = this.renderRoot.querySelector( const contents = this.renderRoot.querySelector<HTMLElement>("#contents");
"#contents", const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
) as HTMLElement;
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay;
if (contents && replay && !this.splitter) { if (contents && replay && !this.splitter) {
const opts = { const opts = {
@@ -378,11 +379,11 @@ class Item extends LitElement {
return; return;
} }
let item = this.loadInfo && this.loadInfo.customColl; // @todo(2023-11-06) update customColl -> customItem let item = this.loadInfo?.customColl; // @todo(2023-11-06) update customColl -> customItem
if (!item) { if (!item) {
const res = await sourceToId(this.sourceUrl); const res = await sourceToId(this.sourceUrl!);
item = res.coll; item = res.item;
} }
this.item = item; this.item = item;
@@ -406,11 +407,7 @@ class Item extends LitElement {
...json, ...json,
}; };
if ( if (this.loadInfo?.extraConfig?.headers) {
this.loadInfo &&
this.loadInfo.extraConfig &&
this.loadInfo.extraConfig.headers
) {
const headers = this.loadInfo.extraConfig.headers; const headers = this.loadInfo.extraConfig.headers;
await fetch(`${itemApiPrefix}/updateAuth`, { await fetch(`${itemApiPrefix}/updateAuth`, {
method: "POST", method: "POST",
@@ -870,10 +867,9 @@ class Item extends LitElement {
const isReplay = !!this.tabData.url; const isReplay = !!this.tabData.url;
const isSidebar = isReplay && this.showSidebar; const isSidebar = isReplay && this.showSidebar;
if (!isReplay && this.tabData && this.tabData.view) { if (!isReplay && this.tabData?.view) {
const detail = { const detail = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme title: this.tabLabels[this.tabData.view],
title: (this.tabLabels as any)[this.tabData.view],
replayTitle: false, replayTitle: false,
}; };
this.dispatchEvent( this.dispatchEvent(
@@ -932,7 +928,7 @@ class Item extends LitElement {
ts="${this.tabData.ts || ""}" ts="${this.tabData.ts || ""}"
@coll-tab-nav="${this.onItemTabNav}" @coll-tab-nav="${this.onItemTabNav}"
id="replay" id="replay"
@replay-loading="${(e) => @replay-loading="${(e: CustomEvent<{ loading: boolean }>) =>
(this.isLoading = e.detail.loading)}" (this.isLoading = e.detail.loading)}"
@replay-favicons="${this.onFavIcons}" @replay-favicons="${this.onFavIcons}"
> >
@@ -1488,14 +1484,14 @@ class Item extends LitElement {
} }
dragStart() { dragStart() {
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
if (replay) { if (replay) {
replay.setDisablePointer(true); replay.setDisablePointer(true);
} }
} }
dragEnd() { dragEnd() {
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
if (replay) { if (replay) {
replay.setDisablePointer(false); replay.setDisablePointer(false);
} }
@@ -1517,7 +1513,7 @@ class Item extends LitElement {
></wr-item-info>`; ></wr-item-info>`;
} }
renderExtraToolbar(/*isDropdown = false*/) { protected renderExtraToolbar(/*isDropdown = false*/): "" | TemplateResult<1> {
return ""; return "";
} }
@@ -1582,9 +1578,7 @@ class Item extends LitElement {
// This is a workaround, since this app's routing doesn't permit normal // This is a workaround, since this app's routing doesn't permit normal
// following of in-page anchors. // following of in-page anchors.
event.preventDefault(); event.preventDefault();
( this.renderRoot.querySelector<HTMLElement>("#skip-replay-target")?.focus();
this.renderRoot.querySelector("#skip-replay-target") as HTMLElement
).focus();
} }
onKeyDown(event) { onKeyDown(event) {
@@ -1702,7 +1696,7 @@ class Item extends LitElement {
onSubmit(event) { onSubmit(event) {
event.preventDefault(); event.preventDefault();
const input = this.renderRoot.querySelector("input") as HTMLInputElement; const input = this.renderRoot.querySelector<HTMLInputElement>("input")!;
if (input.value) { if (input.value) {
this.navigateTo(input.value); this.navigateTo(input.value);
} else { } else {
@@ -1719,12 +1713,12 @@ class Item extends LitElement {
onTimestampDropdownBtnBlur(event: MouseEvent) { onTimestampDropdownBtnBlur(event: MouseEvent) {
const btn = event.currentTarget as HTMLButtonElement; const btn = event.currentTarget as HTMLButtonElement;
const dropdown = btn.closest("sl-dropdown") as SlDropdown; const dropdown = btn.closest<SlDropdown>("sl-dropdown");
const relatedTarget = event.relatedTarget as HTMLElement; const relatedTarget = event.relatedTarget as HTMLElement;
if (relatedTarget && dropdown.contains(relatedTarget)) { if (relatedTarget && dropdown?.contains(relatedTarget)) {
return; return;
} }
if (dropdown.open) { if (dropdown?.open) {
dropdown.hide(); dropdown.hide();
} }
} }
@@ -1741,9 +1735,7 @@ class Item extends LitElement {
data = { url: value }; data = { url: value };
if (value === this.tabData.url) { if (value === this.tabData.url) {
const replay = this.renderRoot.querySelector( const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
"wr-coll-replay",
) as Replay;
if (replay) { if (replay) {
replay.refresh(); replay.refresh();
} }
@@ -1761,8 +1753,7 @@ class Item extends LitElement {
_stringToParams(value) { _stringToParams(value) {
const q = new URLSearchParams(value.slice(RWP_SCHEME.length)); const q = new URLSearchParams(value.slice(RWP_SCHEME.length));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme const data: Partial<URLResource> = {};
const data: any = {};
data.url = ""; data.url = "";
data.ts = ""; data.ts = "";
@@ -1807,7 +1798,7 @@ class Item extends LitElement {
this.menuActive = false; this.menuActive = false;
if (this.tabData.url) { if (this.tabData.url) {
const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
if (replay) { if (replay) {
replay.refresh(); replay.refresh();
} }
+72 -62
View File
@@ -5,7 +5,7 @@ import prettyBytes from "pretty-bytes";
import { parseURLSchemeHostPath } from "./pageutils"; import { parseURLSchemeHostPath } from "./pageutils";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import { LoadInfo } from "./item"; import type { LoadInfo } from "./item";
// =========================================================================== // ===========================================================================
class Loader extends LitElement { class Loader extends LitElement {
@@ -27,7 +27,8 @@ class Loader extends LitElement {
@property({ type: String }) swName?: string; @property({ type: String }) swName?: string;
pingInterval: number | NodeJS.Timer = 0; pingInterval: number | NodeJS.Timer = 0;
fileHandle: FileSystemHandle | null = null; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch
fileHandle: any = null;
noWebWorker = false; noWebWorker = false;
worker?: Worker | null; worker?: Worker | null;
@@ -54,57 +55,71 @@ class Loader extends LitElement {
this.worker = navigator.serviceWorker as unknown as Worker; this.worker = navigator.serviceWorker as unknown as Worker;
} }
this.worker.addEventListener("message", (event) => { this.worker.addEventListener(
switch (event.data.msg_type) { "message",
case "collProgress": (
if (event.data.name === this.coll) { event: MessageEvent<{
this.percent = event.data.percent; msg_type: string;
if (event.data.error) { name: string;
this.error = event.data.error; percent: number;
this.state = "errored"; error?: string;
this.errorAllowRetry = true; fileHandle: FileSystemHandle | null;
this.fileHandle = event.data.fileHandle; currentSize?: number;
if (this.error === "missing_local_file") { totalSize?: number;
this.tryFileHandle = false; extraMsg?: string;
} else if ( }>,
this.error === "permission_needed" && ) => {
event.data.fileHandle switch (event.data.msg_type) {
) { case "collProgress":
this.state = "permission_needed"; if (event.data.name === this.coll) {
break; this.percent = event.data.percent;
if (event.data.error) {
this.error = event.data.error;
this.state = "errored";
this.errorAllowRetry = true;
this.fileHandle = event.data.fileHandle;
if (this.error === "missing_local_file") {
this.tryFileHandle = false;
} else if (
this.error === "permission_needed" &&
event.data.fileHandle
) {
this.state = "permission_needed";
break;
}
} }
} if (event.data.currentSize && event.data.totalSize) {
if (event.data.currentSize && event.data.totalSize) { this.currentSize = event.data.currentSize;
this.currentSize = event.data.currentSize; this.totalSize = event.data.totalSize;
this.totalSize = event.data.totalSize;
}
this.extraMsg = event.data.extraMsg;
}
break;
case "collAdded":
if (event.data.name === this.coll) {
if (!this.total) {
this.total = 100;
}
this.progress = this.total;
this.percent = 100;
this.dispatchEvent(
new CustomEvent("coll-loaded", { detail: event.data }),
);
if (!this.noWebWorker) {
this.worker?.terminate();
} else {
if (this.pingInterval) {
clearInterval(this.pingInterval);
} }
this.extraMsg = event.data.extraMsg;
} }
this.worker = null; break;
}
break; case "collAdded":
} if (event.data.name === this.coll) {
}); if (!this.total) {
this.total = 100;
}
this.progress = this.total;
this.percent = 100;
this.dispatchEvent(
new CustomEvent("coll-loaded", { detail: event.data }),
);
if (!this.noWebWorker) {
this.worker?.terminate();
} else {
if (this.pingInterval) {
clearInterval(this.pingInterval);
}
}
this.worker = null;
}
break;
}
},
);
} }
async doLoad() { async doLoad() {
@@ -115,7 +130,7 @@ class Loader extends LitElement {
// const noSWError = getSWErrorMsg(); // const noSWError = getSWErrorMsg();
if (this.loadInfo && this.loadInfo.swError) { if (this.loadInfo?.swError) {
this.state = "errored"; this.state = "errored";
this.error = this.loadInfo.swError; this.error = this.loadInfo.swError;
this.errorAllowRetry = false; this.errorAllowRetry = false;
@@ -124,7 +139,7 @@ class Loader extends LitElement {
// custom protocol handlers here... // custom protocol handlers here...
try { try {
const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl); const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl!);
switch (scheme) { switch (scheme) {
case "googledrive": case "googledrive":
@@ -179,11 +194,7 @@ You can select a file to upload from the main page by clicking the 'Choose File.
extraConfig = this.loadInfo.extraConfig; extraConfig = this.loadInfo.extraConfig;
} }
// todo: too special case? // todo: too special case?
if ( if (sourceUrl!.startsWith("proxy:") && extraConfig?.recording) {
sourceUrl!.startsWith("proxy:") &&
extraConfig &&
extraConfig.recording
) {
// @ts-expect-error - TS2322 - Type '"recordingproxy"' is not assignable to type 'undefined'. // @ts-expect-error - TS2322 - Type '"recordingproxy"' is not assignable to type 'undefined'.
type = "recordingproxy"; type = "recordingproxy";
} }
@@ -229,7 +240,7 @@ You can select a file to upload from the main page by clicking the 'Choose File.
return this._gdWait; return this._gdWait;
} }
async onLoadReady(event) { onLoadReady(event: CustomEvent) {
if (this._gdResolve) { if (this._gdResolve) {
//const digest = await digestMessage(url, 'SHA-256'); //const digest = await digestMessage(url, 'SHA-256');
//this.coll = "id-" + digest.slice(0, 12); //this.coll = "id-" + digest.slice(0, 12);
@@ -250,7 +261,7 @@ You can select a file to upload from the main page by clicking the 'Choose File.
return; return;
} }
if (navigator.serviceWorker && navigator.serviceWorker.controller) { if (navigator.serviceWorker?.controller) {
navigator.serviceWorker.controller.postMessage(msg); navigator.serviceWorker.controller.postMessage(msg);
if (this.pingInterval) { if (this.pingInterval) {
clearInterval(this.pingInterval); clearInterval(this.pingInterval);
@@ -260,7 +271,7 @@ You can select a file to upload from the main page by clicking the 'Choose File.
updated(changedProperties: PropertyValues<this>) { updated(changedProperties: PropertyValues<this>) {
if ( if (
(this.sourceUrl && changedProperties.has("sourceUrl")) || Boolean(this.sourceUrl && changedProperties.has("sourceUrl")) ||
changedProperties.has("tryFileHandle") changedProperties.has("tryFileHandle")
) { ) {
this.doLoad(); this.doLoad();
@@ -410,8 +421,7 @@ You can select a file to upload from the main page by clicking the 'Choose File.
} }
async onAskPermission() { async onAskPermission() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const result = await this.fileHandle?.requestPermission({
const result = await (this.fileHandle as any)?.requestPermission({
mode: "read", mode: "read",
}); });
if (result === "granted") { if (result === "granted") {
+3 -4
View File
@@ -1,10 +1,9 @@
"use strict"; "use strict";
import { LitElement, html, css, unsafeCSS } from "lit"; import { LitElement, html, css, unsafeCSS, type CSSResultGroup } from "lit";
import { unsafeSVG } from "lit/directives/unsafe-svg.js"; import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import { styleMap } from "lit/directives/style-map.js"; import { styleMap } from "lit/directives/style-map.js";
// @ts-expect-error - TS2307 - Cannot find module '../assets/main.scss' or its corresponding type declarations.
import allCssRaw from "../assets/main.scss"; import allCssRaw from "../assets/main.scss";
import rwpLogo from "../assets/logo.svg"; import rwpLogo from "../assets/logo.svg";
@@ -14,7 +13,7 @@ const replayPrefix = "./w";
// =========================================================================== // ===========================================================================
const allCss = unsafeCSS(allCssRaw); const allCss = unsafeCSS(allCssRaw);
function wrapCss(custom) { function wrapCss(custom: CSSResultGroup): CSSResultGroup {
return [allCss, custom]; return [allCss, custom];
} }
@@ -22,7 +21,7 @@ const IS_APP =
// @ts-expect-error - TS2339 - Property 'IS_APP' does not exist on type 'Window & typeof globalThis'. // @ts-expect-error - TS2339 - Property 'IS_APP' does not exist on type 'Window & typeof globalThis'.
window.IS_APP || window.IS_APP ||
// @ts-expect-error - TS2551 - Property 'electron' does not exist on type 'Window & typeof globalThis'. Did you mean 'Electron'? | TS2551 - Property 'electron' does not exist on type 'Window & typeof globalThis'. Did you mean 'Electron'? // @ts-expect-error - TS2551 - Property 'electron' does not exist on type 'Window & typeof globalThis'. Did you mean 'Electron'? | TS2551 - Property 'electron' does not exist on type 'Window & typeof globalThis'. Did you mean 'Electron'?
(window.electron && window.electron.IS_APP) || window.electron?.IS_APP ||
window.matchMedia("(display-mode: standalone)").matches; window.matchMedia("(display-mode: standalone)").matches;
const VERSION = __VERSION__; const VERSION = __VERSION__;
+21 -17
View File
@@ -8,6 +8,7 @@ import "keyword-mark-element/lib/keyword-mark.js";
import { getReplayLink } from "./pageutils"; import { getReplayLink } from "./pageutils";
import { wrapCss } from "./misc"; import { wrapCss } from "./misc";
import type { URLResource } from "./types";
// =========================================================================== // ===========================================================================
class PageEntry extends LitElement { class PageEntry extends LitElement {
@@ -18,8 +19,7 @@ class PageEntry extends LitElement {
textSnippet: string | null = ""; textSnippet: string | null = "";
@property({ type: Object }) @property({ type: Object })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme page: URLResource | null = null;
page: any = null;
@property({ type: String }) @property({ type: String })
replayPrefix = ""; replayPrefix = "";
@@ -191,8 +191,8 @@ class PageEntry extends LitElement {
} }
render() { render() {
const p = this.page; const p = this.page!;
const date = this.page.date; const date = this.page!.date;
const hasSize = typeof p.size === "number"; const hasSize = typeof p.size === "number";
@@ -231,8 +231,8 @@ class PageEntry extends LitElement {
@click="${this.onReplay}" @click="${this.onReplay}"
href="${getReplayLink( href="${getReplayLink(
"pages", "pages",
this.page.url, this.page!.url,
this.page.timestamp, this.page!.timestamp!,
)}" )}"
> >
<p class="is-size-6 has-text-weight-bold has-text-link text"> <p class="is-size-6 has-text-weight-bold has-text-link text">
@@ -289,19 +289,23 @@ class PageEntry extends LitElement {
return html`<img return html`<img
class="thumbnail" class="thumbnail"
@error=${() => (this.thumbnailValid = false)} @error=${() => (this.thumbnailValid = false)}
src=${`${this.replayPrefix}/${this.page.timestamp}id_/urn:thumbnail:${this.page.url}`} src=${`${this.replayPrefix}/${this.page!.timestamp}id_/urn:thumbnail:${
this.page!.url
}`}
loading="lazy" loading="lazy"
/>`; />`;
} }
private renderFavicon() { private renderFavicon() {
if (!this.iconValid || !this.page.favIconUrl) { if (!this.iconValid || !this.page!.favIconUrl) {
return; return;
} }
return html`<img return html`<img
class="favicon" class="favicon"
@error=${() => (this.iconValid = false)} @error=${() => (this.iconValid = false)}
src=${`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`} src=${`${this.replayPrefix}/${this.page!.timestamp}id_/${
this.page!.favIconUrl
}`}
loading="lazy" loading="lazy"
/>`; />`;
} }
@@ -309,13 +313,13 @@ class PageEntry extends LitElement {
updateSnippet() { updateSnippet() {
const oldVal = this.textSnippet; const oldVal = this.textSnippet;
if (!this.query || !this.page.text) { if (!this.query || !this.page!.text) {
this.textSnippet = null; this.textSnippet = null;
this.requestUpdate("textSnippet", oldVal); this.requestUpdate("textSnippet", oldVal);
return; return;
} }
let textContent = this.page.text; let textContent = this.page!.text;
let query = this.query; let query = this.query;
let inx = textContent.indexOf(this.query); let inx = textContent.indexOf(this.query);
@@ -351,22 +355,22 @@ class PageEntry extends LitElement {
this.requestUpdate("textSnippet", oldVal); this.requestUpdate("textSnippet", oldVal);
} }
onReplay(event, reload = false) { onReplay(event: Event, reload = false) {
event.preventDefault(); event.preventDefault();
const data = { const data = {
url: this.page.url, url: this.page!.url,
ts: this.page.timestamp, ts: this.page!.timestamp,
}; };
this.sendChangeEvent(data, reload); this.sendChangeEvent(data, reload);
return false; return false;
} }
onReload(event) { onReload(event: Event) {
return this.onReplay(event, true); return this.onReplay(event, true);
} }
sendChangeEvent(data, reload) { sendChangeEvent(data, reload: boolean) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("coll-tab-nav", { new CustomEvent("coll-tab-nav", {
bubbles: true, bubbles: true,
@@ -382,7 +386,7 @@ class PageEntry extends LitElement {
} }
onSendSelToggle(event) { onSendSelToggle(event) {
const page = this.page.id; const page = this.page!.id;
const selected = event.currentTarget.checked; const selected = event.currentTarget.checked;
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("sel-page", { detail: { page, selected } }), new CustomEvent("sel-page", { detail: { page, selected } }),
+91 -89
View File
@@ -1,7 +1,8 @@
"use strict"; "use strict";
import { LitElement, html, css, unsafeCSS } from "lit"; import { LitElement, html, css, unsafeCSS, type PropertyValues } from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { wrapCss, clickOnSpacebarPress } from "./misc"; import { wrapCss, clickOnSpacebarPress } from "./misc";
import ndjson from "fetch-ndjson"; import ndjson from "fetch-ndjson";
@@ -16,31 +17,28 @@ import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.svg";
import type { Sorter } from "./sorter"; import type { Sorter } from "./sorter";
import type { PageEntry } from "./pageentry"; import type { PageEntry } from "./pageentry";
import type { Id, Index } from "flexsearch";
import type { ItemType, URLResource } from "./types";
// =========================================================================== // ===========================================================================
class Pages extends LitElement { class Pages extends LitElement {
@property({ type: Array }) @property({ type: Array })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme filteredPages: URLResource[] = [];
filteredPages: any[] = [];
@property({ type: Array }) @property({ type: Array })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme sortedPages: URLResource[] = [];
sortedPages: any[] = [];
@property({ type: String }) @property({ type: String })
query = ""; query = "";
@property() @property({ attribute: false })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme flex: Index | null = null;
flex: any = null;
@property({ attribute: false })
textPages: URLResource[] | null = null;
@property() @property()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme newQuery: string | null = null;
textPages: any = null;
@property()
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme
newQuery: any = null;
@property({ type: Boolean }) @property({ type: Boolean })
loading = false; loading = false;
@@ -55,7 +53,7 @@ class Pages extends LitElement {
hasExtraPages = false; hasExtraPages = false;
@property({ type: Number }) @property({ type: Number })
currList: number = 0; currList = 0;
@property({ type: Boolean }) @property({ type: Boolean })
active = false; active = false;
@@ -67,7 +65,7 @@ class Pages extends LitElement {
changeNeeded = false; changeNeeded = false;
@property({ attribute: false }) @property({ attribute: false })
selectedPages = new Set(); selectedPages = new Set<number>();
@property({ type: Boolean }) @property({ type: Boolean })
menuActive = false; menuActive = false;
@@ -88,20 +86,16 @@ class Pages extends LitElement {
ts = ""; ts = "";
@property({ type: Boolean }) @property({ type: Boolean })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme editing = false;
editing: any = false;
@property({ type: Array })
toDeletePages: Set<number> | number[] | null = null;
@property({ type: Object }) @property({ type: Object })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme toDeletePage: URLResource | null = null;
toDeletePages: any = null;
@property({ type: Object }) @property({ type: Object })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme collInfo: ItemType | Record<string, never> | null = null;
toDeletePage: any = null;
@property({ type: Object })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme
collInfo: any;
@property({ type: Boolean }) @property({ type: Boolean })
allSelected = false; allSelected = false;
@@ -109,8 +103,7 @@ class Pages extends LitElement {
@property({ type: String }) @property({ type: String })
defaultKey = ""; defaultKey = "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme private _ival: number | undefined;
private _ival: any;
static get sortKeys() { static get sortKeys() {
return [ return [
@@ -137,8 +130,7 @@ class Pages extends LitElement {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme async updated(changedProperties: PropertyValues<this>) {
async updated(changedProperties: Map<string, any>) {
if (changedProperties.has("collInfo")) { if (changedProperties.has("collInfo")) {
this.updateTextSearch(); this.updateTextSearch();
} else if (changedProperties.has("query")) { } else if (changedProperties.has("query")) {
@@ -163,7 +155,8 @@ class Pages extends LitElement {
this.sortKey = "date"; this.sortKey = "date";
this.sortDesc = true; this.sortDesc = true;
} }
const sorter = this.renderRoot.querySelector("wr-sorter") as Sorter; const sorter =
this.renderRoot.querySelector<Sorter<URLResource>>("wr-sorter");
if (sorter) { if (sorter) {
sorter.sortKey = this.sortKey; sorter.sortKey = this.sortKey;
sorter.sortDesc = this.sortDesc; sorter.sortDesc = this.sortDesc;
@@ -174,21 +167,19 @@ class Pages extends LitElement {
//if (await this.updateComplete) { //if (await this.updateComplete) {
const selected = this.renderRoot.querySelector(".current"); const selected = this.renderRoot.querySelector(".current");
if (selected) { if (selected) {
const opts = { const opts: ScrollIntoViewOptions = {
behavior: "smooth", behavior: "smooth",
block: "nearest", block: "nearest",
inline: "nearest", inline: "nearest",
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme setTimeout(() => selected.scrollIntoView(opts), 100);
setTimeout(() => selected.scrollIntoView(opts as any), 100);
} }
//} //}
} }
} }
onChangeQuery(event: Event) { onChangeQuery(event: Event) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme this.newQuery = (event.currentTarget as HTMLInputElement).value;
this.newQuery = (event.currentTarget as any).value;
//this.loading = true; //this.loading = true;
if (this._ival) { if (this._ival) {
window.clearTimeout(this._ival); window.clearTimeout(this._ival);
@@ -206,11 +197,13 @@ class Pages extends LitElement {
this.loading = true; this.loading = true;
if (this.flex && this.query && this.textPages) { if (this.flex && this.query && this.textPages) {
const results = await this.flex.searchAsync(this.query, 25); const results = await this.flex.searchAsync(this.query, 25);
this.filteredPages = results.map((inx: number) => this.textPages[inx]); this.filteredPages = results.map(
(inx: Id) => this.textPages![inx as number],
);
} else if (this.showAllPages && this.hasExtraPages) { } else if (this.showAllPages && this.hasExtraPages) {
this.filteredPages = [...this.textPages]; this.filteredPages = [...this.textPages!];
} else { } else {
this.filteredPages = [...this.collInfo.pages]; this.filteredPages = [...this.collInfo!.pages];
} }
if (this.currList !== 0) { if (this.currList !== 0) {
@@ -220,6 +213,9 @@ class Pages extends LitElement {
// normalize the date // normalize the date
for (const page of this.filteredPages) { for (const page of this.filteredPages) {
const { timestamp, date } = getPageDateTS(page); const { timestamp, date } = getPageDateTS(page);
if (date == null) {
throw new Error("Page date is null");
}
page.timestamp = timestamp; page.timestamp = timestamp;
page.date = date; page.date = date;
} }
@@ -232,7 +228,7 @@ class Pages extends LitElement {
async filterCurated() { async filterCurated() {
const resp = await fetch( const resp = await fetch(
`${this.collInfo.apiPrefix}/curated/${this.currList}`, `${this.collInfo!.apiPrefix}/curated/${this.currList}`,
); );
const json = await resp.json(); const json = await resp.json();
@@ -251,11 +247,11 @@ class Pages extends LitElement {
this.flex = flex; this.flex = flex;
this.textPages = pages; this.textPages = pages;
this.hasExtraPages = this.hasExtraPages = Boolean(
this.textPages && this.textPages &&
this.collInfo && this.collInfo?.pages &&
this.collInfo.pages && this.textPages.length > this.collInfo.pages.length,
this.textPages.length > this.collInfo.pages.length; );
return Promise.all( return Promise.all(
pages.map((page, index) => { pages.map((page, index) => {
@@ -280,9 +276,9 @@ class Pages extends LitElement {
let count = 0; let count = 0;
try { try {
const cache = await caches.open("cache:" + this.collInfo.coll); const cache = await caches.open("cache:" + this.collInfo!.coll);
const indexUrl = `${this.collInfo.apiPrefix}/textIndex`; const indexUrl = `${this.collInfo!.apiPrefix}/textIndex`;
let resp = await cache.match(indexUrl); let resp = await cache.match(indexUrl);
@@ -293,8 +289,7 @@ class Pages extends LitElement {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme const lines: unknown[] = [];
const lines: any[] = [];
for await (const line of ndjson(resp.body!.getReader())) { for await (const line of ndjson(resp.body!.getReader())) {
if (!line.url) { if (!line.url) {
@@ -310,7 +305,7 @@ class Pages extends LitElement {
console.warn(e); console.warn(e);
} finally { } finally {
if (count === 0) { if (count === 0) {
await this.addPages(this.collInfo.pages); await this.addPages(this.collInfo!.pages);
} }
this.updatingSearch = false; this.updatingSearch = false;
@@ -577,7 +572,7 @@ class Pages extends LitElement {
role="heading" role="heading"
aria-level="${this.isSidebar ? "2" : "1"}" aria-level="${this.isSidebar ? "2" : "1"}"
> >
Pages in ${this.collInfo.title} Pages in ${this.collInfo!.title}
</div> </div>
<div class="search-bar notification is-marginless"> <div class="search-bar notification is-marginless">
${this.isSidebar ${this.isSidebar
@@ -612,7 +607,7 @@ class Pages extends LitElement {
<input <input
id="titleEdit" id="titleEdit"
class="input" class="input"
value="${this.collInfo.title}" value="${ifDefined(this.collInfo!.title)}"
@blur="${this.onUpdateTitle}" @blur="${this.onUpdateTitle}"
/> />
</form> </form>
@@ -621,14 +616,14 @@ class Pages extends LitElement {
class="index-bar-title" class="index-bar-title"
@dblclick="${() => (this.editing = true)}" @dblclick="${() => (this.editing = true)}"
> >
${this.collInfo.name || this.collInfo.title} ${this.collInfo!.name || this.collInfo!.title}
</div> </div>
${this.collInfo.description ${this.collInfo!.description
? html`<div ? html`<div
class="index-bar-description" class="index-bar-description"
@dblclick="${() => (this.editing = true)}" @dblclick="${() => (this.editing = true)}"
> >
${this.collInfo.description} ${this.collInfo!.description}
</div>` </div>`
: html``}`} : html``}`}
${this.editable ${this.editable
@@ -638,8 +633,10 @@ class Pages extends LitElement {
? html` <span class="check-select"> ? html` <span class="check-select">
<label class="checkbox"> <label class="checkbox">
<input <input
@change=${(e) => @change=${(e: Event) =>
(this.showAllPages = e.currentTarget.checked)} (this.showAllPages = (
e.currentTarget as HTMLInputElement
).checked)}
type="checkbox" type="checkbox"
.checked="${this.showAllPages}" .checked="${this.showAllPages}"
/> />
@@ -659,7 +656,7 @@ class Pages extends LitElement {
${this.renderDownloadMenu()} ${this.renderDownloadMenu()}
</div>` </div>`
: ""} : ""}
${this.collInfo.lists.length ${this.collInfo!.lists.length
? html` ? html`
<p id="filter-label" class="menu-label">Filter By List:</p> <p id="filter-label" class="menu-label">Filter By List:</p>
<div class="index-bar-menu menu"> <div class="index-bar-menu menu">
@@ -673,7 +670,7 @@ class Pages extends LitElement {
><i>All Pages</i></a ><i>All Pages</i></a
> >
</li> </li>
${this.collInfo.lists.map( ${this.collInfo!.lists.map(
(list) => (list) =>
html` <li> html` <li>
<a <a
@@ -859,7 +856,12 @@ class Pages extends LitElement {
: html` : html`
<p> <p>
Are you sure you want to delete the Are you sure you want to delete the
<b>${this.toDeletePages.size}</b> selected pages? <b
>${Array.isArray(this.toDeletePages)
? new Set(this.toDeletePage).size
: this.toDeletePages.size}</b
>
selected pages?
</p> </p>
`} `}
<p>This operation can not be undone.</p> <p>This operation can not be undone.</p>
@@ -914,7 +916,7 @@ class Pages extends LitElement {
pid="${p.id}" pid="${p.id}"
@sel-page="${this.onSelectToggle}" @sel-page="${this.onSelectToggle}"
@delete-page="${this.onDeleteConfirm}" @delete-page="${this.onDeleteConfirm}"
replayPrefix="${this.collInfo.replayPrefix}" replayPrefix="${this.collInfo!.replayPrefix}"
query="${this.query}" query="${this.query}"
class="${this.isSidebar ? "sidebar" : ""}" class="${this.isSidebar ? "sidebar" : ""}"
> >
@@ -934,10 +936,8 @@ class Pages extends LitElement {
if (!this.editable) { if (!this.editable) {
return; return;
} }
const input = this.renderRoot.querySelector( const input = this.renderRoot.querySelector<HTMLInputElement>("#titleEdit");
"#titleEdit", if (!input?.value.trim()) {
) as HTMLInputElement;
if (!input || !input.value.trim()) {
return; return;
} }
@@ -945,15 +945,16 @@ class Pages extends LitElement {
const body = JSON.stringify({ title }); const body = JSON.stringify({ title });
fetch(`${this.collInfo.apiPrefix}/metadata`, { method: "POST", body }).then( fetch(`${this.collInfo!.apiPrefix}/metadata`, {
(res) => { method: "POST",
if (res.status === 200) { body,
this.dispatchEvent( }).then((res) => {
new CustomEvent("coll-update", { detail: { title } }), if (res.status === 200) {
); this.dispatchEvent(
} new CustomEvent("coll-update", { detail: { title } }),
}, );
); }
});
} }
onMenu(event) { onMenu(event) {
@@ -1025,22 +1026,23 @@ class Pages extends LitElement {
selected ? Array.from(this.selectedPages.keys()).join(",") : "all", selected ? Array.from(this.selectedPages.keys()).join(",") : "all",
); );
params.set("format", format); params.set("format", format);
if (this.collInfo.filename) { if (this.collInfo!.filename) {
params.set("filename", this.collInfo.filename); params.set("filename", this.collInfo!.filename);
} }
window.location.href = `${this.collInfo.apiPrefix}/dl?` + params.toString(); window.location.href =
`${this.collInfo!.apiPrefix}/dl?` + params.toString();
} }
onDeleteConfirm(event) { onDeleteConfirm(event: Event) {
const page = event.currentTarget.page; const page = (event.currentTarget as PageEntry).page;
// if page is one of the selected, delete entire selection // if page is one of the selected, delete entire selection
if (this.selectedPages.has(page.id)) { if (this.selectedPages.has(page!.id)) {
this.toDeletePages = this.selectedPages; this.toDeletePages = this.selectedPages;
this.toDeletePage = null; this.toDeletePage = null;
} else { } else {
// else, delete just the page // else, delete just the page
this.toDeletePages = [page.id]; this.toDeletePages = [page!.id];
this.toDeletePage = page; this.toDeletePage = page;
} }
} }
@@ -1048,18 +1050,18 @@ class Pages extends LitElement {
async onDeletePages() { async onDeletePages() {
const pageMap = {}; const pageMap = {};
for (const id of this.toDeletePages) { for (const id of this.toDeletePages!) {
const p = this.renderRoot.querySelector( const p = this.renderRoot.querySelector<PageEntry>(
`wr-page-entry[pid="${id}"]`, `wr-page-entry[pid="${id}"]`,
) as PageEntry; );
if (p) { if (p) {
p.deleting = true; p.deleting = true;
pageMap[id] = p; pageMap[id] = p;
} }
} }
for (const id of this.toDeletePages) { for (const id of this.toDeletePages!) {
const resp = await fetch(`${this.collInfo.apiPrefix}/page/${id}`, { const resp = await fetch(`${this.collInfo!.apiPrefix}/page/${id}`, {
method: "DELETE", method: "DELETE",
}); });
const json = await resp.json(); const json = await resp.json();
@@ -1075,12 +1077,12 @@ class Pages extends LitElement {
continue; continue;
} }
const inx = this.collInfo.pages.indexOf(page); const inx = this.collInfo!.pages.indexOf(page);
if (inx < 0) { if (inx < 0) {
continue; continue;
} }
this.collInfo.pages.splice(inx, 1); this.collInfo!.pages.splice(inx, 1);
} }
this.toDeletePages = null; this.toDeletePages = null;
@@ -1110,7 +1112,7 @@ class Pages extends LitElement {
} }
getNoResultsMessage() { getNoResultsMessage() {
if (!this.collInfo || !this.collInfo.pages.length) { if (!this.collInfo?.pages.length) {
return html`<span class="fix-text-wrapping" return html`<span class="fix-text-wrapping"
>No Pages are defined in this archive. The archive may be empty. >No Pages are defined in this archive. The archive may be empty.
<a href="#view=resources">Try browsing by URL</a>.</span <a href="#view=resources">Try browsing by URL</a>.</span
@@ -1140,7 +1142,7 @@ class Pages extends LitElement {
const diff = const diff =
element.scrollHeight - element.scrollTop - element.clientHeight; element.scrollHeight - element.scrollTop - element.clientHeight;
if (diff < 40) { if (diff < 40) {
const sorter = this.renderRoot.querySelector("wr-sorter") as Sorter; const sorter = this.renderRoot.querySelector<Sorter>("wr-sorter");
if (sorter) { if (sorter) {
sorter.getMore(); sorter.getMore();
} }
+12 -10
View File
@@ -1,5 +1,7 @@
import type { URLResource } from "./types";
// =========================================================================== // ===========================================================================
async function digestMessage(message, hashtype) { async function digestMessage(message: string, hashtype: AlgorithmIdentifier) {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
const hashBuffer = await crypto.subtle.digest(hashtype, msgUint8); // hash the message const hashBuffer = await crypto.subtle.digest(hashtype, msgUint8); // hash the message
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
@@ -10,7 +12,7 @@ async function digestMessage(message, hashtype) {
} }
// =========================================================================== // ===========================================================================
function tsToDate(ts) { function tsToDate(ts: string | null | undefined) {
if (!ts) { if (!ts) {
return ""; return "";
} }
@@ -37,7 +39,7 @@ function tsToDate(ts) {
} }
// =========================================================================== // ===========================================================================
function getDateFromTS(ts: number) { function getDateFromTS(ts: string | number) {
let date: Date | null = null; let date: Date | null = null;
date = new Date(ts); date = new Date(ts);
const timestamp = const timestamp =
@@ -46,7 +48,7 @@ function getDateFromTS(ts: number) {
} }
// =========================================================================== // ===========================================================================
function getPageDateTS(page) { function getPageDateTS(page: URLResource) {
let date: Date | null = null; let date: Date | null = null;
try { try {
date = new Date(page.ts || page.date); date = new Date(page.ts || page.date);
@@ -60,12 +62,12 @@ function getPageDateTS(page) {
} }
// =========================================================================== // ===========================================================================
function getTS(iso) { function getTS(iso: string) {
return iso.replace(/[-:T]/g, "").slice(0, 14); return iso.replace(/[-:T]/g, "").slice(0, 14);
} }
// =========================================================================== // ===========================================================================
function getReplayLink(view, url, ts) { function getReplayLink(view: string, url: string, ts: string) {
const params = new URLSearchParams(); const params = new URLSearchParams();
params.set("view", view); params.set("view", view);
params.set("url", url); params.set("url", url);
@@ -74,7 +76,7 @@ function getReplayLink(view, url, ts) {
} }
// =========================================================================== // ===========================================================================
async function sourceToId(url) { async function sourceToId(url: string) {
try { try {
new URL(url); new URL(url);
} catch (e) { } catch (e) {
@@ -83,14 +85,14 @@ async function sourceToId(url) {
} }
const digest = await digestMessage(url, "SHA-256"); const digest = await digestMessage(url, "SHA-256");
const coll = "id-" + digest.slice(0, 12); const item = "id-" + digest.slice(0, 12);
return { url, coll }; return { url, item };
} }
// =========================================================================== // ===========================================================================
// simple parse of scheme, host, rest of path // simple parse of scheme, host, rest of path
// not using URL due to different browser behavior // not using URL due to different browser behavior
function parseURLSchemeHostPath(url) { function parseURLSchemeHostPath(url: string) {
let inx = url.indexOf("://"); let inx = url.indexOf("://");
let hostInx = 0; let hostInx = 0;
let scheme = ""; let scheme = "";
+11 -17
View File
@@ -2,16 +2,15 @@ import { LitElement, html, css, type PropertyValues } from "lit";
import { property } from "lit/decorators.js"; import { property } from "lit/decorators.js";
import { wrapCss, rwpLogo } from "./misc"; import { wrapCss, rwpLogo } from "./misc";
import type { Item } from "./types"; import type { ItemType } from "./types";
// =========================================================================== // ===========================================================================
class Replay extends LitElement { class Replay extends LitElement {
@property({ type: Object }) @property({ type: Object })
collInfo: Item | Record<string, never> | null = null; collInfo: ItemType | Record<string, never> | null = null;
@property({ type: String }) @property({ type: String })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme sourceUrl: string | null = null;
sourceUrl: any = null;
// external url set from parent // external url set from parent
@property({ type: String }) @property({ type: String })
@@ -34,14 +33,13 @@ class Replay extends LitElement {
title = ""; title = "";
@property({ type: String }) @property({ type: String })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme iframeUrl: string | null = null;
iframeUrl: any = null;
@property({ type: Boolean }) @property({ type: Boolean })
showAuth = false; showAuth = false;
@property({ type: Object }) @property({ type: Object })
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme // eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch
authFileHandle: any = null; authFileHandle: any = null;
private reauthWait: null | Promise<void> = null; private reauthWait: null | Promise<void> = null;
@@ -164,8 +162,7 @@ class Replay extends LitElement {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme setDisablePointer(disable: boolean) {
setDisablePointer(disable: any) {
const iframe = this.renderRoot.querySelector("iframe"); const iframe = this.renderRoot.querySelector("iframe");
if (iframe) { if (iframe) {
@@ -173,8 +170,7 @@ class Replay extends LitElement {
} }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme onReplayMessage(event: MessageEvent) {
onReplayMessage(event: any) {
const iframe = this.renderRoot.querySelector("iframe"); const iframe = this.renderRoot.querySelector("iframe");
if (iframe && event.source === iframe.contentWindow) { if (iframe && event.source === iframe.contentWindow) {
@@ -238,14 +234,12 @@ class Replay extends LitElement {
this._loadPoll = window.setInterval(() => { this._loadPoll = window.setInterval(() => {
const iframe = this.renderRoot.querySelector("iframe"); const iframe = this.renderRoot.querySelector("iframe");
if ( if (
!iframe || !iframe?.contentDocument ||
!iframe.contentDocument ||
!iframe.contentWindow || !iframe.contentWindow ||
(iframe.contentDocument.readyState === "complete" && (iframe.contentDocument.readyState === "complete" &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme !(iframe.contentWindow as Window & { _WBWombat: unknown })._WBWombat)
!(iframe.contentWindow as Window & { _WBWombat: any })._WBWombat)
) { ) {
this.clearLoading(iframe && iframe.contentWindow); this.clearLoading(iframe?.contentWindow);
} }
}, 5000); }, 5000);
} }
@@ -405,7 +399,7 @@ class Replay extends LitElement {
</button> </button>
` `
: html` <wr-gdrive : html` <wr-gdrive
.sourceUrl="${this.sourceUrl}" .sourceUrl="${this.sourceUrl!}"
state="trymanual" state="trymanual"
.reauth="${true}" .reauth="${true}"
@load-ready="${this.onReAuthed}" @load-ready="${this.onReAuthed}"
+23 -32
View File
@@ -1,44 +1,37 @@
import { LitElement, html, css, type PropertyValues } from "lit"; import { LitElement, html, css, type PropertyValues } from "lit";
import { customElement, property } from "lit/decorators.js";
import { wrapCss } from "./misc"; import { wrapCss } from "./misc";
import fasSortDown from "@fortawesome/fontawesome-free/svgs/solid/sort-down.svg"; import fasSortDown from "@fortawesome/fontawesome-free/svgs/solid/sort-down.svg";
import fasSortUp from "@fortawesome/fontawesome-free/svgs/solid/sort-up.svg"; import fasSortUp from "@fortawesome/fontawesome-free/svgs/solid/sort-up.svg";
// =========================================================================== // ===========================================================================
@customElement("wr-sorter")
class Sorter<T = unknown> extends LitElement { class Sorter<T = unknown> extends LitElement {
sortedData: T[]; @property({ attribute: false })
data: T[]; sortedData: T[] = [];
pageResults: number;
numResults: number;
sortKey: string | null; @property({ attribute: false })
sortDesc: boolean | null; data: T[] = [];
sortKeys: { key: string; name: string }[] = [];
constructor() {
super();
this.sortedData = [];
this.data = [];
this.pageResults = 0; @property({ type: String })
this.numResults = 0; id!: string;
this.sortKey = null; @property({ type: Number })
this.sortDesc = null; pageResults = 0;
}
static get properties() { @property({ attribute: false })
return { numResults = 0;
id: { type: String },
pageResults: { type: Number }, @property({ type: String })
sortKey: string | null = null;
data: { type: Array }, @property({ type: Boolean })
sortedData: { type: Array }, sortDesc: boolean | null = null;
sortKey: { type: String }, @property({ attribute: false })
sortDesc: { type: Boolean }, sortKeys?: { key: string; name: string }[];
};
}
firstUpdated() { firstUpdated() {
if (this.id) { if (this.id) {
@@ -75,7 +68,7 @@ class Sorter<T = unknown> extends LitElement {
this.sortedData = [...this.data]; this.sortedData = [...this.data];
this.numResults = this.pageResults; this.numResults = this.pageResults;
if ((this.sortKey == null || this.sortKey) === "") { if (this.sortKey === "") {
if (this.sortDesc) { if (this.sortDesc) {
this.sortedData.reverse(); this.sortedData.reverse();
} }
@@ -129,10 +122,10 @@ class Sorter<T = unknown> extends LitElement {
render() { render() {
return html` return html`
<div class="select is-small"> <div class="select is-small">
<select id="sort-select" @change=${(e) => <select id="sort-select" @change=${(e: Event) =>
(this.sortKey = e.currentTarget.value)}> (this.sortKey = (e.currentTarget as HTMLSelectElement).value)}>
${this.sortKeys.map( ${this.sortKeys?.map(
(sort) => html` (sort) => html`
<option value="${sort.key}" ?selected="${sort.key === this.sortKey}"> <option value="${sort.key}" ?selected="${sort.key === this.sortKey}">
Sort By: ${sort.name} Sort By: ${sort.name}
@@ -154,6 +147,4 @@ class Sorter<T = unknown> extends LitElement {
} }
} }
customElements.define("wr-sorter", Sorter);
export { Sorter }; export { Sorter };
+64 -88
View File
@@ -7,52 +7,37 @@ import { marked } from "marked";
import { getTS, getReplayLink } from "./pageutils"; import { getTS, getReplayLink } from "./pageutils";
import Split from "split.js"; import Split from "split.js";
import { ItemInfo } from "./item-info"; import { type ItemType } from "./types";
import { customElement, property } from "lit/decorators.js";
// =========================================================================== // ===========================================================================
@customElement("wr-coll-story")
class Story extends LitElement { class Story extends LitElement {
collInfo: ItemInfo | Record<string, never> | null; @property({ type: Object })
curatedPageMap: Record<string, unknown[]>; collInfo: ItemType | Record<string, never> | null = null;
currList: number;
active: boolean;
lastST: number;
clickTime: number;
isSidebar: boolean;
splitDirection: boolean | "vertical" | "horizontal";
constructor() {
super();
this.collInfo = null; @property({ type: Object })
curatedPageMap: Record<string, unknown[]> = {};
this.curatedPageMap = {}; @property({ type: Number })
currList = 0;
this.currList = 0; @property({ type: Boolean })
active = false;
this.active = false; @property({ type: Boolean })
isSidebar = false;
this.lastST = 0; @property({ type: Boolean })
this.clickTime = 0; splitDirection: boolean | "vertical" | "horizontal" = false;
this.isSidebar = false; lastST = 0;
this.splitDirection = false; clickTime = 0;
}
static get properties() { private obs!: ResizeObserver;
return { private splitter: Split.Instance | null = null;
collInfo: { type: Object },
active: { type: Boolean }, recalcSplitter(width: number) {
curatedPageMap: { type: Object },
currList: { type: Number },
isSidebar: { type: Boolean },
splitDirection: { type: Boolean },
};
}
recalcSplitter(width) {
this.splitDirection = this.splitDirection =
this.isSidebar || width < 769 ? "vertical" : "horizontal"; this.isSidebar || width < 769 ? "vertical" : "horizontal";
} }
@@ -60,12 +45,10 @@ class Story extends LitElement {
firstUpdated() { firstUpdated() {
this.recalcSplitter(document.documentElement.clientWidth); this.recalcSplitter(document.documentElement.clientWidth);
// @ts-expect-error - TS2339 - Property 'obs' does not exist on type 'Story'.
this.obs = new ResizeObserver((entries /*, observer*/) => { this.obs = new ResizeObserver((entries /*, observer*/) => {
this.recalcSplitter(entries[0].contentRect.width); this.recalcSplitter(entries[0].contentRect.width);
}); });
// @ts-expect-error - TS2339 - Property 'obs' does not exist on type 'Story'.
this.obs.observe(this); this.obs.observe(this);
} }
@@ -93,47 +76,43 @@ class Story extends LitElement {
} }
configureSplitter() { configureSplitter() {
const sidebar = this.renderRoot.querySelector(".sidebar"); const sidebar = this.renderRoot.querySelector<HTMLElement>(".sidebar");
const content = this.renderRoot.querySelector(".main-content"); const content = this.renderRoot.querySelector<HTMLElement>(".main-content");
// @ts-expect-error - TS2339 - Property 'splitter' does not exist on type 'Story'.
if (this.splitter) { if (this.splitter) {
try { try {
// @ts-expect-error - TS2339 - Property 'splitter' does not exist on type 'Story'.
this.splitter.destroy(); this.splitter.destroy();
} catch (e) { } catch (e) {
// ignore splitter destory err // ignore splitter destory err
} }
// @ts-expect-error - TS2339 - Property 'splitter' does not exist on type 'Story'.
this.splitter = null; this.splitter = null;
} }
// @ts-expect-error - TS2339 - Property 'splitter' does not exist on type 'Story'.
if (sidebar && content && !this.splitter) { if (sidebar && content && !this.splitter) {
const opts = { const opts: Split.Options = {
sizes: [20, 80], sizes: [20, 80],
gutterSize: 4, gutterSize: 4,
direction: this.splitDirection, direction: this.splitDirection as "horizontal" | "vertical",
}; };
// @ts-expect-error - TS2339 - Property 'splitter' does not exist on type 'Story'. | TS2322 - Type 'Element' is not assignable to type 'string | HTMLElement'. | TS2322 - Type 'Element' is not assignable to type 'string | HTMLElement'.
this.splitter = Split([sidebar, content], opts); this.splitter = Split([sidebar, content], opts);
} }
} }
async doLoadCurated() { async doLoadCurated() {
if (this.collInfo == null) {
return;
}
this.curatedPageMap = {}; this.curatedPageMap = {};
const pageMap = {}; const pageMap = {};
// @ts-expect-error - TS2339 - Property 'collInfo' does not exist on type 'Story'.
for (const page of this.collInfo.pages) { for (const page of this.collInfo.pages) {
pageMap[page.id] = page; pageMap[page.id] = page;
} }
// @ts-expect-error - TS2339 - Property 'collInfo' does not exist on type 'Story'.
for (const curated of this.collInfo.curatedPages) { for (const curated of this.collInfo.curatedPages) {
if (!this.curatedPageMap[curated.list]) { if (!this.curatedPageMap[curated.list]) {
this.curatedPageMap[curated.list] = []; this.curatedPageMap[curated.list] = [];
@@ -289,29 +268,21 @@ class Story extends LitElement {
? "is-active" ? "is-active"
: ""} menu-label is-size-4" : ""} menu-label is-size-4"
@click=${this.onClickScroll} @click=${this.onClickScroll}
>${ >${this.collInfo?.title}</a
// @ts-expect-error - TS2339 - Property 'collInfo' does not exist on type 'Story'.
this.collInfo.title
}</a
> >
<ul class="menu-list"> <ul class="menu-list">
${ ${this.collInfo?.lists.map(
// @ts-expect-error - TS2339 - Property 'collInfo' does not exist on type 'Story'. (list) =>
this.collInfo.lists.map( html` <li>
(list) => <a
html` <li> @click=${this.onClickScroll}
<a href="#list-${list.id}"
@click=${this.onClickScroll} data-list="${list.id}"
href="#list-${list.id}" class="${currListNum === list.id ? "is-active" : ""}"
data-list="${list.id}" >${list.title}</a
class="${currListNum === list.id >
? "is-active" </li>`,
: ""}" )}
>${list.title}</a
>
</li>`,
)
}
</ul> </ul>
</li> </li>
</ul> </ul>
@@ -338,8 +309,7 @@ class Story extends LitElement {
} }
renderLists() { renderLists() {
// @ts-expect-error - TS2339 - Property 'collInfo' does not exist on type 'Story'. return html` ${this.collInfo?.lists?.map(
return html` ${this.collInfo.lists.map(
(list) => html` (list) => html`
<article id="list-${list.id}"> <article id="list-${list.id}">
<div class="content"> <div class="content">
@@ -380,11 +350,11 @@ class Story extends LitElement {
</li>`; </li>`;
} }
onReplay(event) { onReplay(event: Event) {
event.preventDefault(); event.preventDefault();
const data = { const data = {
url: event.currentTarget.getAttribute("data-url"), url: (event.currentTarget as Element).getAttribute("data-url"),
ts: event.currentTarget.getAttribute("data-ts"), ts: (event.currentTarget as Element).getAttribute("data-ts"),
}; };
this.sendChangeEvent(data); this.sendChangeEvent(data);
return false; return false;
@@ -404,30 +374,35 @@ class Story extends LitElement {
scrollToList() { scrollToList() {
// lists are 1 based, 0 is header, 1 is first list // lists are 1 based, 0 is header, 1 is first list
// @ts-expect-error - TS2339 - Property 'currList' does not exist on type 'Story'. | TS2339 - Property 'collInfo' does not exist on type 'Story'. if (this.currList > (this.collInfo?.lists?.length ?? 0)) {
if (this.currList > this.collInfo.lists.length) {
this.currList = 0; this.currList = 0;
} }
const opts = { behavior: "smooth", block: "nearest", inline: "nearest" }; const opts: ScrollIntoViewOptions = {
behavior: "smooth",
block: "nearest",
inline: "nearest",
};
this.clickTime = new Date().getTime(); this.clickTime = new Date().getTime();
// @ts-expect-error - TS2339 - Property 'getElementById' does not exist on type 'HTMLElement | ShadowRoot'. | TS2339 - Property 'currList' does not exist on type 'Story'. const curr = (this.renderRoot as ShadowRoot).getElementById(
const curr = this.renderRoot.getElementById("list-" + this.currList); "list-" + this.currList,
);
if (curr) { if (curr) {
curr.scrollIntoView(opts); curr.scrollIntoView(opts);
} }
} }
onScroll(event) { onScroll(event: Event) {
const scrollable = event.currentTarget; const scrollable = event.currentTarget as HTMLElement;
// @ts-expect-error - TS2339 - Property 'getElementById' does not exist on type 'HTMLElement | ShadowRoot'. | TS2339 - Property 'currList' does not exist on type 'Story'. const curr = (this.renderRoot as ShadowRoot).getElementById(
const curr = this.renderRoot.getElementById("list-" + this.currList); "list-" + this.currList,
);
if (!curr) { if (!curr) {
return; return;
} }
let next = curr; let next: Element = curr;
const target = scrollable.offsetTop; const target = scrollable.offsetTop;
const currST = scrollable.scrollTop; const currST = scrollable.scrollTop;
@@ -461,13 +436,14 @@ class Story extends LitElement {
`a[data-list="${this.currList}"]`, `a[data-list="${this.currList}"]`,
); );
if (sel) { if (sel) {
const opts = { behavior: "smooth", block: "nearest", inline: "nearest" }; const opts: ScrollIntoViewOptions = {
// @ts-expect-error - TS2345 - Argument of type '{ behavior: string; block: string; inline: string; }' is not assignable to parameter of type 'boolean | ScrollIntoViewOptions | undefined'. behavior: "smooth",
block: "nearest",
inline: "nearest",
};
sel.scrollIntoView(opts); sel.scrollIntoView(opts);
} }
} }
} }
customElements.define("wr-coll-story", Story);
export { Story }; export { Story };
-29
View File
@@ -1,29 +0,0 @@
import INDEX_HTML from "../index.html";
import { SWReplay } from "@webrecorder/wabac/src/swmain";
import { WorkerLoader } from "@webrecorder/wabac/src/loaders";
if (self.registration) {
const staticData = new Map();
const prefix = self.registration.scope;
staticData.set(prefix, { type: "text/html", content: INDEX_HTML });
staticData.set(prefix + "index.html", {
type: "text/html",
content: INDEX_HTML,
});
const sp = new URLSearchParams(self.location.search);
const defaultConfig = {};
if (sp.get("ruffle") == "1") {
defaultConfig.injectScripts = ["ruffle/ruffle.js"];
}
self.sw = new SWReplay({ staticData, defaultConfig });
} else {
new WorkerLoader(self);
}
+22 -6
View File
@@ -1,4 +1,21 @@
export interface Item { export type URLResource = {
id: number;
title: string;
url: string;
mime: string;
status: string;
date: Date;
ts: string;
desc?: string;
timestamp?: string;
size: number;
favIconUrl: string;
text?: string;
};
export type Page = URLResource;
export type ItemType = {
filename: string; filename: string;
sourceUrl: string; sourceUrl: string;
replayPrefix: string; replayPrefix: string;
@@ -26,11 +43,10 @@ export interface Item {
software?: string; software?: string;
}; };
onDemand?: boolean; onDemand?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme pages: URLResource[];
pages?: any[]; curatedPages: (URLResource & { list: string })[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme lists: URLResource[];
lists?: any[];
ctime?: string; ctime?: string;
totalSize?: unknown; totalSize?: unknown;
size?: number | string; size?: number | string;
} };
+2 -9
View File
@@ -6,15 +6,8 @@ import { getReplayLink } from "./pageutils";
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg"; import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import "keyword-mark-element/lib/keyword-mark.js"; import "keyword-mark-element/lib/keyword-mark.js";
import { type Item as ItemType } from "./types"; import { type ItemType } from "./types";
import { type URLResource } from "./types";
type URLResource = {
url: string;
mime: string;
status: string;
date: string;
ts: string;
};
// =========================================================================== // ===========================================================================
/** /**
+3 -1
View File
@@ -4,7 +4,9 @@ export function assertNonNullish<T>(
): asserts item is NonNullable<T> { ): asserts item is NonNullable<T> {
if (item == null) { if (item == null) {
throw new Error( throw new Error(
`Variable \`${itemName}\` was expected to be defined, but found value ${item} instead`, `Variable \`${itemName}\` was expected to be defined, but found value ${
item as Exclude<null | undefined, T>
} (${typeof item}) instead`,
); );
} }
} }
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"noEmit": true
},
"extends": "./tsconfig.json",
"include": ["**/*.ts", "**/*.js", ".*.js"],
"exclude": ["__generated__", "__mocks__", "dist", "ruffle/*.js"]
}
+4 -1
View File
@@ -6,6 +6,9 @@
"moduleResolution": "node", "moduleResolution": "node",
"allowJs": true, "allowJs": true,
"strict": true, "strict": true,
"declaration": true,
"declarationMap": true,
"declarationDir": "./dist/types",
"noImplicitAny": false, "noImplicitAny": false,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,
@@ -34,5 +37,5 @@
], ],
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src/**/*.ts", "src/**/*.js"] "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.d.ts"]
} }
+2 -1
View File
@@ -98,8 +98,9 @@ const electronPreloadConfig = (/*env, argv*/) => {
const browserConfig = (/*env, argv*/) => { const browserConfig = (/*env, argv*/) => {
const isDevServer = process.env.WEBPACK_SERVE; const isDevServer = process.env.WEBPACK_SERVE;
/** @type {import('webpack').Configuration['entry']} */
const entry = { const entry = {
ui: "./src/ui.ts", ui: "./src/index.ts",
}; };
const patterns = [{ from: "package.json", to: "_data/package.json" }]; const patterns = [{ from: "package.json", to: "_data/package.json" }];
+272 -1006
View File
File diff suppressed because it is too large Load Diff