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
This commit is contained in:
sua yoo
2023-09-05 17:26:16 -07:00
parent d8300169ec
commit 73e7da4c7c
7 changed files with 210 additions and 157 deletions
+13
View File
@@ -134,3 +134,16 @@ a:focus {
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; 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;
}
+4
View File
@@ -29,6 +29,7 @@
"stream-browserify": "^3.0.0" "stream-browserify": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/flexsearch": "^0.7.3",
"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",
@@ -177,5 +178,8 @@
}, },
"lint-staged": { "lint-staged": {
"*.ts": "prettier --write" "*.ts": "prettier --write"
},
"prettier": {
"trailingComma": "all"
} }
} }
+75 -74
View File
@@ -1,6 +1,7 @@
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { LitElement, html, css, unsafeCSS } from "lit"; import { LitElement, html, css, unsafeCSS } from "lit";
import { property, state } from "lit/decorators.js";
import "keyword-mark-element/lib/keyword-mark.js"; import "keyword-mark-element/lib/keyword-mark.js";
@@ -10,35 +11,41 @@ import { wrapCss } from "./misc";
// =========================================================================== // ===========================================================================
class PageEntry extends LitElement { class PageEntry extends LitElement {
constructor() { @property({ type: String })
super(); query = "";
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;
}
static get properties() { @property({ type: String })
return { textSnippet: string | null = "";
query: { type: String },
textSnippet: { type: String }, @property({ type: Object })
page: { type: Object }, page: any = null;
replayPrefix: { type: String },
deleting: { type: Boolean }, @property({ type: String })
selected: { type: Boolean }, replayPrefix = "";
editable: { type: Boolean },
iconValid: { type: Boolean }, @property({ type: Boolean })
index: { type: Number }, deleting = false;
isCurrent: { type: Boolean },
isSidebar: { type: Boolean }, @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() { static get styles() {
return wrapCss(css` return wrapCss(css`
@@ -79,17 +86,28 @@ class PageEntry extends LitElement {
} }
.favicon { .favicon {
width: 24px !important;
height: 24px !important;
display: inline-block; display: inline-block;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
img.favicon {
.media-left .favicon {
width: 2rem;
height: 2rem;
}
.media-left img.favicon {
filter: drop-shadow(1px 1px 2px grey); filter: drop-shadow(1px 1px 2px grey);
} }
.media-content .favicon {
width: 1.15rem;
height: 1.15rem;
margin: 0 0.25rem;
}
.media-left { .media-left {
width: 6rem;
align-self: center; align-self: center;
text-align: center;
} }
.delete-button { .delete-button {
@@ -148,10 +166,7 @@ class PageEntry extends LitElement {
display: inline; display: inline;
} }
${prefix} .col-index { ${prefix} .col-index {
position: absolute; display: none;
top: 0px;
left: 0px;
margin-top: -0.75em;
} }
${prefix} .columns { ${prefix} .columns {
display: flex; display: flex;
@@ -161,14 +176,15 @@ class PageEntry extends LitElement {
display: initial !important; display: initial !important;
font-style: italic; font-style: italic;
} }
${prefix} .media-left {
padding-left: 0.75rem;
}
`; `;
} }
updated(changedProperties) { updated(changedProperties) {
if (changedProperties.has("page") || changedProperties.has("query")) { if (changedProperties.has("page") || changedProperties.has("query")) {
this.updateSnippet(); this.updateSnippet();
this.iconValid = !!this.page.favIconUrl;
//this.updateFavIcon();
this.deleting = false; this.deleting = false;
} }
} }
@@ -206,18 +222,7 @@ class PageEntry extends LitElement {
</div> </div>
<div class="column"> <div class="column">
<div class="media"> <div class="media">
<figure class="media-left"> <figure class="media-left">${this.renderPageIcon()}</figure>
<p class="">
${this.iconValid
? html` <img
class="favicon"
@error="${() => (this.iconValid = false)}"
src="${this.replayPrefix}/${this.page
.timestamp}id_/${p.favIconUrl}"
/>`
: html` <span class="favicon"></span>`}
</p>
</figure>
<div class="media-content ${this.isCurrent ? "current" : ""}"> <div class="media-content ${this.isCurrent ? "current" : ""}">
<div role="heading" aria-level="${this.isSidebar ? "4" : "3"}"> <div role="heading" aria-level="${this.isSidebar ? "4" : "3"}">
<a <a
@@ -237,7 +242,7 @@ class PageEntry extends LitElement {
<p class="has-text-dark text"> <p class="has-text-dark text">
<keyword-mark keywords="${this.query}" <keyword-mark keywords="${this.query}"
>${p.url}</keyword-mark >${p.url}</keyword-mark
> >${this.thumbnailValid ? this.renderFavicon() : ""}
</p> </p>
<p class="has-text-grey-dark text is-inline-date"> <p class="has-text-grey-dark text is-inline-date">
${date ? date.toLocaleString() : ""} ${date ? date.toLocaleString() : ""}
@@ -276,32 +281,28 @@ class PageEntry extends LitElement {
`; `;
} }
async updateFavIcon() { private renderPageIcon() {
if (!this.page.favIconUrl) { if (!this.thumbnailValid) {
this.favIconData = null; return this.renderFavicon();
}
return html`<img
class="thumbnail"
@error=${() => (this.thumbnailValid = false)}
src=${`${this.replayPrefix}/${this.page.timestamp}id_/urn:thumbnail:${this.page.url}`}
loading="lazy"
/>`;
}
private renderFavicon() {
if (!this.iconValid || !this.page.favIconUrl) {
return; return;
} }
return html`<img
const resp = await fetch( class="favicon"
`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`, @error=${() => (this.iconValid = false)}
); src=${`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`}
loading="lazy"
if (resp.status != 200) { />`;
this.favIconData = null;
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;
}
} }
updateSnippet() { updateSnippet() {
+108 -81
View File
@@ -1,6 +1,7 @@
"use strict"; "use strict";
import { LitElement, html, css, unsafeCSS } from "lit"; import { LitElement, html, css, unsafeCSS } from "lit";
import { property, state } from "lit/decorators.js";
import { wrapCss, clickOnSpacebarPress } from "./misc"; import { wrapCss, clickOnSpacebarPress } from "./misc";
import ndjson from "fetch-ndjson"; 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 fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import fasAngleDown from "@fortawesome/fontawesome-free/svgs/solid/angle-down.svg"; import fasAngleDown from "@fortawesome/fontawesome-free/svgs/solid/angle-down.svg";
import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.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 { class Pages extends LitElement {
constructor() { @property({ type: Array })
super(); filteredPages: any[] = [];
this.filteredPages = [];
this.sortedPages = [];
this.query = "";
this.flex = null;
this.textPages = null;
this.newQuery = null;
this.loading = false;
this.updatingSearch = false;
this.showAllPages = false; @property({ type: Array })
this.hasExtraPages = false; sortedPages: any[] = [];
this.currList = 0; @property({ type: String })
query = "";
this.active = false; @property()
this.editable = false; flex: any = null;
this.changeNeeded = false;
this.selectedPages = new Set(); @property()
textPages: any = null;
this.menuActive = false; @property()
newQuery: any = null;
this.sortKey = "date"; @property({ type: Boolean })
this.sortDesc = true; loading = false;
this.isSidebar = false; @property({ type: Boolean })
this.url = ""; updatingSearch = false;
this.ts = "";
this.editing = false; @property({ type: Boolean })
showAllPages = false;
this.toDeletePages = null; @property({ type: Boolean })
this.toDeletePage = null; 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() { static get sortKeys() {
return [ 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() { _timedUpdate() {
if (this.newQuery !== null) { if (this.newQuery !== null) {
this.query = this.newQuery; this.query = this.newQuery;
@@ -114,7 +127,7 @@ class Pages extends LitElement {
} }
} }
async updated(changedProperties) { 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")) {
@@ -139,7 +152,7 @@ class Pages extends LitElement {
this.sortKey = "date"; this.sortKey = "date";
this.sortDesc = true; this.sortDesc = true;
} }
const sorter = this.renderRoot.querySelector("wr-sorter"); const sorter = this.renderRoot.querySelector("wr-sorter") as Sorter;
if (sorter) { if (sorter) {
sorter.sortKey = this.sortKey; sorter.sortKey = this.sortKey;
sorter.sortDesc = this.sortDesc; sorter.sortDesc = this.sortDesc;
@@ -155,14 +168,14 @@ class Pages extends LitElement {
block: "nearest", block: "nearest",
inline: "nearest", inline: "nearest",
}; };
setTimeout(() => selected.scrollIntoView(opts), 100); setTimeout(() => selected.scrollIntoView(opts as any), 100);
} }
//} //}
} }
} }
onChangeQuery(event) { onChangeQuery(event: Event) {
this.newQuery = event.currentTarget.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);
@@ -180,7 +193,7 @@ 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) => this.textPages[inx]); this.filteredPages = results.map((inx: number) => this.textPages[inx]);
} else if (this.showAllPages && this.hasExtraPages) { } else if (this.showAllPages && this.hasExtraPages) {
this.filteredPages = [...this.textPages]; this.filteredPages = [...this.textPages];
} else { } 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) { if (!line.url) {
continue; continue;
} }
@@ -324,7 +337,7 @@ class Pages extends LitElement {
} }
.header .column.pagetitle { .header .column.pagetitle {
margin-left: 2.5em; padding-left: 0.25em;
} }
.column.main-content { .column.main-content {
@@ -336,6 +349,12 @@ class Pages extends LitElement {
margin-left: 0.75em; margin-left: 0.75em;
} }
.thumbnail {
width: 6rem;
flex: 0 0 auto;
box-sizing: content-box;
}
.index-bar { .index-bar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -769,13 +788,17 @@ class Pages extends LitElement {
: ""}" : ""}"
>Date</a >Date</a
> >
<div class="column thumbnail">
<span class="sr-only">Page thumbnail or favicon</span>
</div>
<a <a
role="button" role="button"
href="#" href="#"
@click="${this.onSort}" @click="${this.onSort}"
@keyup="${clickOnSpacebarPress}" @keyup="${clickOnSpacebarPress}"
data-key="title" data-key="title"
class="column is-6 pagetitle ${this.sortKey === "title" class="column is-6 pagetitle ${this.query ? "is-5" : "is-6"} ${this
.sortKey === "title"
? this.sortDesc ? this.sortDesc
? "desc" ? "desc"
: "asc" : "asc"
@@ -869,10 +892,10 @@ class Pages extends LitElement {
return html` <li class="page-entry ${selected ? "selected" : ""}"> return html` <li class="page-entry ${selected ? "selected" : ""}">
<wr-page-entry <wr-page-entry
.index="${this.query || this.isSidebar ? i + 1 : 0}" .index="${this.query || this.isSidebar ? i + 1 : 0}"
.editable="${this.editable}" ?editable="${this.editable}"
.selected="${selected}" ?selected="${selected}"
.isCurrent="${this.isCurrPage(p)}" ?isCurrent="${this.isCurrPage(p)}"
.isSidebar="${this.isSidebar}" ?isSidebar="${this.isSidebar}"
.page="${p}" .page="${p}"
pid="${p.id}" pid="${p.id}"
@sel-page="${this.onSelectToggle}" @sel-page="${this.onSelectToggle}"
@@ -897,7 +920,9 @@ class Pages extends LitElement {
if (!this.editable) { if (!this.editable) {
return; return;
} }
const input = this.renderRoot.querySelector("#titleEdit"); const input = this.renderRoot.querySelector(
"#titleEdit",
) as HTMLInputElement;
if (!input || !input.value.trim()) { if (!input || !input.value.trim()) {
return; return;
} }
@@ -1010,7 +1035,9 @@ class Pages extends LitElement {
const pageMap = {}; const pageMap = {};
for (const id of this.toDeletePages) { for (const id of this.toDeletePages) {
const p = this.renderRoot.querySelector(`wr-page-entry[pid="${id}"]`); const p = this.renderRoot.querySelector(
`wr-page-entry[pid="${id}"]`,
) as PageEntry;
if (p) { if (p) {
p.deleting = true; p.deleting = true;
pageMap[id] = p; pageMap[id] = p;
@@ -1099,7 +1126,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"); const sorter = this.renderRoot.querySelector("wr-sorter") as Sorter;
if (sorter) { if (sorter) {
sorter.getMore(); sorter.getMore();
} }
+4 -1
View File
@@ -11,7 +11,10 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"importHelpers": true, "importHelpers": true,
"sourceMap": true, "sourceMap": true,
"inlineSources": true "inlineSources": true,
"paths": {
"flexsearch": ["./node_modules/@types/flexsearch"]
}
}, },
"include": [ "include": [
"src/**/*.ts" "src/**/*.ts"
+5
View File
@@ -509,6 +509,11 @@
"@types/qs" "*" "@types/qs" "*"
"@types/serve-static" "*" "@types/serve-static" "*"
"@types/flexsearch@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@types/flexsearch/-/flexsearch-0.7.3.tgz#ee79b1618035c82284278e05652e91116765b634"
integrity sha512-HXwADeHEP4exXkCIwy2n1+i0f1ilP1ETQOH5KDOugjkTFZPntWo0Gr8stZOaebkxsdx+k0X/K6obU/+it07ocg==
"@types/fs-extra@^9.0.11": "@types/fs-extra@^9.0.11":
version "9.0.12" version "9.0.12"
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.12.tgz#9b8f27973df8a7a3920e8461517ebf8a7d4fdfaf" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.12.tgz#9b8f27973df8a7a3920e8461517ebf8a7d4fdfaf"