From 8753cf52f9113de87c53fc320d4559db4108d27f Mon Sep 17 00:00:00 2001 From: sua yoo Date: Tue, 5 Sep 2023 16:20:01 -0700 Subject: [PATCH] Show page thumbnails in pages list (#221) * convert pages / pageentry to ts * render page icon * adjust for smaller screen * show favicon even if thumbnail exists --- assets/main.scss | 13 ++ package.json | 4 + src/{pageentry.js => pageentry.ts} | 145 +++++++++++----------- src/{pages.js => pages.ts} | 189 ++++++++++++++++------------- src/shoelace.js | 2 +- tsconfig.json | 5 +- yarn.lock | 5 + 7 files changed, 208 insertions(+), 155 deletions(-) rename src/{pageentry.js => pageentry.ts} (79%) rename src/{pages.js => pages.ts} (90%) diff --git a/assets/main.scss b/assets/main.scss index 53d149d1..63bac8e6 100644 --- a/assets/main.scss +++ b/assets/main.scss @@ -134,3 +134,16 @@ a:focus { box-sizing: border-box; overflow: hidden; } + +// Make available to screen readers while hiding element visually: +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} \ No newline at end of file diff --git a/package.json b/package.json index 82bcfa00..638299e9 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "stream-browserify": "^3.0.0" }, "devDependencies": { + "@types/flexsearch": "^0.7.3", "copy-webpack-plugin": "^9.0.1", "css-loader": "^6.2.0", "electron": "^25.1.1", @@ -177,5 +178,8 @@ }, "lint-staged": { "*.ts": "prettier --write" + }, + "prettier": { + "trailingComma": "all" } } diff --git a/src/pageentry.js b/src/pageentry.ts similarity index 79% rename from src/pageentry.js rename to src/pageentry.ts index 5514c385..a477a8d8 100644 --- a/src/pageentry.js +++ b/src/pageentry.ts @@ -1,6 +1,7 @@ import prettyBytes from "pretty-bytes"; import { LitElement, html, css, unsafeCSS } from "lit"; +import { property, state } from "lit/decorators.js"; import "keyword-mark-element/lib/keyword-mark.js"; @@ -10,35 +11,41 @@ import { wrapCss } from "./misc"; // =========================================================================== class PageEntry extends LitElement { - constructor() { - super(); - this.query = ""; - this.textSnippet = ""; - this.page = null; - this.replayPrefix = ""; - this.deleting = false; - this.editable = false; - this.iconValid = false; - this.index = 0; - this.isCurrent = false; - this.isSidebar = false; - } + @property({ type: String }) + query = ""; - static get properties() { - return { - query: { type: String }, - textSnippet: { type: String }, - page: { type: Object }, - replayPrefix: { type: String }, - deleting: { type: Boolean }, - selected: { type: Boolean }, - editable: { type: Boolean }, - iconValid: { type: Boolean }, - index: { type: Number }, - isCurrent: { type: Boolean }, - isSidebar: { type: Boolean }, - }; - } + @property({ type: String }) + textSnippet: string | null = ""; + + @property({ type: Object }) + page: any = null; + + @property({ type: String }) + replayPrefix = ""; + + @property({ type: Boolean }) + deleting = false; + + @property({ type: Boolean }) + selected = false; + + @property({ type: Boolean }) + editable = false; + + @property({ type: Number }) + index = 0; + + @property({ type: Boolean }) + isCurrent = false; + + @property({ type: Boolean }) + isSidebar = false; + + @state() + thumbnailValid = true; + + @state() + iconValid = true; static get styles() { return wrapCss(css` @@ -79,17 +86,28 @@ class PageEntry extends LitElement { } .favicon { - width: 24px !important; - height: 24px !important; display: inline-block; vertical-align: text-bottom; } - img.favicon { + + .media-left .favicon { + width: 2rem; + height: 2rem; + } + .media-left img.favicon { filter: drop-shadow(1px 1px 2px grey); } + .media-content .favicon { + width: 1.15rem; + height: 1.15rem; + margin: 0 0.25rem; + } + .media-left { + width: 6rem; align-self: center; + text-align: center; } .delete-button { @@ -148,10 +166,7 @@ class PageEntry extends LitElement { display: inline; } ${prefix} .col-index { - position: absolute; - top: 0px; - left: 0px; - margin-top: -0.75em; + display: none; } ${prefix} .columns { display: flex; @@ -161,14 +176,15 @@ class PageEntry extends LitElement { display: initial !important; font-style: italic; } + ${prefix} .media-left { + padding-left: 0.75rem; + } `; } updated(changedProperties) { if (changedProperties.has("page") || changedProperties.has("query")) { this.updateSnippet(); - this.iconValid = !!this.page.favIconUrl; - //this.updateFavIcon(); this.deleting = false; } } @@ -206,18 +222,7 @@ class PageEntry extends LitElement {
-
-

- ${this.iconValid - ? html` ` - : html` `} -

-
+
${this.renderPageIcon()}
${p.url} + >${this.thumbnailValid ? this.renderFavicon() : ""}

${date ? date.toLocaleString() : ""} @@ -276,32 +281,28 @@ class PageEntry extends LitElement { `; } - async updateFavIcon() { - if (!this.page.favIconUrl) { - this.favIconData = null; - return; + private renderPageIcon() { + if (!this.thumbnailValid) { + return this.renderFavicon(); } + return html` (this.thumbnailValid = false)} + src=${`${this.replayPrefix}/${this.page.timestamp}id_/urn:thumbnail:${this.page.url}`} + loading="lazy" + />`; + } - const resp = await fetch( - `${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`, - ); - - if (resp.status != 200) { - this.favIconData = null; + private renderFavicon() { + if (!this.iconValid || !this.page.favIconUrl) { return; } - - const payload = await resp.arrayBuffer(); - const mime = resp.headers.get("content-type"); - - try { - this.favIconData = `data:${mime};base64,${btoa( - String.fromCharCode.apply(null, payload), - )}`; - } catch (e) { - console.log(e); - this.favIconData = null; - } + return html` (this.iconValid = false)} + src=${`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`} + loading="lazy" + />`; } updateSnippet() { diff --git a/src/pages.js b/src/pages.ts similarity index 90% rename from src/pages.js rename to src/pages.ts index 762e510a..44f19ac0 100644 --- a/src/pages.js +++ b/src/pages.ts @@ -1,6 +1,7 @@ "use strict"; import { LitElement, html, css, unsafeCSS } from "lit"; +import { property, state } from "lit/decorators.js"; import { wrapCss, clickOnSpacebarPress } from "./misc"; import ndjson from "fetch-ndjson"; @@ -11,47 +12,95 @@ import { getTS, getPageDateTS } from "./pageutils"; import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg"; import fasAngleDown from "@fortawesome/fontawesome-free/svgs/solid/angle-down.svg"; - import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.svg"; +import type { Sorter } from "./sorter"; +import type { PageEntry } from "./pageentry"; + // =========================================================================== class Pages extends LitElement { - constructor() { - super(); - this.filteredPages = []; - this.sortedPages = []; - this.query = ""; - this.flex = null; - this.textPages = null; - this.newQuery = null; - this.loading = false; - this.updatingSearch = false; + @property({ type: Array }) + filteredPages: any[] = []; - this.showAllPages = false; - this.hasExtraPages = false; + @property({ type: Array }) + sortedPages: any[] = []; - this.currList = 0; + @property({ type: String }) + query = ""; - this.active = false; - this.editable = false; - this.changeNeeded = false; + @property() + flex: any = null; - this.selectedPages = new Set(); + @property() + textPages: any = null; - this.menuActive = false; + @property() + newQuery: any = null; - this.sortKey = "date"; - this.sortDesc = true; + @property({ type: Boolean }) + loading = false; - this.isSidebar = false; - this.url = ""; - this.ts = ""; + @property({ type: Boolean }) + updatingSearch = false; - this.editing = false; + @property({ type: Boolean }) + showAllPages = false; - this.toDeletePages = null; - this.toDeletePage = null; - } + @property({ type: Boolean }) + hasExtraPages = false; + + @property({ type: Number }) + currList: number = 0; + + @property({ type: Boolean }) + active = false; + + @property({ type: Boolean }) + editable = false; + + @property({ type: Boolean }) + changeNeeded = false; + + @property({ type: Set }) + selectedPages = new Set(); + + @property({ type: Boolean }) + menuActive = false; + + @property({ type: String }) + sortKey = "date"; + + @property({ type: Boolean }) + sortDesc = true; + + @property({ type: Boolean }) + isSidebar = false; + + @property({ type: String }) + url = ""; + + @property({ type: String }) + ts = ""; + + @property({ type: Boolean }) + editing: any = false; + + @property({ type: Object }) + toDeletePages: any = null; + + @property({ type: Object }) + toDeletePage: any = null; + + @property({ type: Object }) + collInfo: any; + + @property({ type: Boolean }) + allSelected = false; + + @property({ type: String }) + defaultKey = ""; + + private _ival: any; static get sortKeys() { return [ @@ -70,42 +119,6 @@ class Pages extends LitElement { ]; } - static get properties() { - return { - active: { type: Boolean }, - collInfo: { type: Object }, - currList: { type: Number }, - filteredPages: { type: Array }, - sortedPages: { type: Array }, - - showAllPages: { type: Boolean }, - - query: { type: String }, - defaultKey: { type: String }, - - loading: { type: Boolean }, - updatingSearch: { type: Boolean }, - editable: { type: Boolean }, - - selectedPages: { type: Set }, - allSelected: { type: Boolean }, - - menuActive: { type: Boolean }, - - sortKey: { type: String }, - sortDesc: { type: Boolean }, - - isSidebar: { type: Boolean }, - url: { type: String }, - ts: { type: String }, - - editing: { type: Boolean }, - - toDeletePages: { type: Object }, - toDeletePage: { type: Object }, - }; - } - _timedUpdate() { if (this.newQuery !== null) { this.query = this.newQuery; @@ -114,7 +127,7 @@ class Pages extends LitElement { } } - async updated(changedProperties) { + async updated(changedProperties: Map) { if (changedProperties.has("collInfo")) { this.updateTextSearch(); } else if (changedProperties.has("query")) { @@ -139,7 +152,7 @@ class Pages extends LitElement { this.sortKey = "date"; this.sortDesc = true; } - const sorter = this.renderRoot.querySelector("wr-sorter"); + const sorter = this.renderRoot.querySelector("wr-sorter") as Sorter; if (sorter) { sorter.sortKey = this.sortKey; sorter.sortDesc = this.sortDesc; @@ -155,14 +168,14 @@ class Pages extends LitElement { block: "nearest", inline: "nearest", }; - setTimeout(() => selected.scrollIntoView(opts), 100); + setTimeout(() => selected.scrollIntoView(opts as any), 100); } //} } } - onChangeQuery(event) { - this.newQuery = event.currentTarget.value; + onChangeQuery(event: Event) { + this.newQuery = (event.currentTarget as any).value; //this.loading = true; if (this._ival) { window.clearTimeout(this._ival); @@ -180,7 +193,7 @@ class Pages extends LitElement { this.loading = true; if (this.flex && this.query && this.textPages) { const results = await this.flex.searchAsync(this.query, 25); - this.filteredPages = results.map((inx) => this.textPages[inx]); + this.filteredPages = results.map((inx: number) => this.textPages[inx]); } else if (this.showAllPages && this.hasExtraPages) { this.filteredPages = [...this.textPages]; } else { @@ -267,9 +280,9 @@ class Pages extends LitElement { } } - const lines = []; + const lines: any[] = []; - for await (const line of ndjson(resp.body.getReader())) { + for await (const line of ndjson(resp.body!.getReader())) { if (!line.url) { continue; } @@ -324,7 +337,7 @@ class Pages extends LitElement { } .header .column.pagetitle { - margin-left: 2.5em; + padding-left: 0.25em; } .column.main-content { @@ -336,6 +349,12 @@ class Pages extends LitElement { margin-left: 0.75em; } + .thumbnail { + width: 6rem; + flex: 0 0 auto; + box-sizing: content-box; + } + .index-bar { display: flex; flex-direction: column; @@ -769,13 +788,17 @@ class Pages extends LitElement { : ""}" >Date +

+ Page thumbnail or favicon +