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
+75 -74
View File
@@ -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 {
</div>
<div class="column">
<div class="media">
<figure class="media-left">
<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>
<figure class="media-left">${this.renderPageIcon()}</figure>
<div class="media-content ${this.isCurrent ? "current" : ""}">
<div role="heading" aria-level="${this.isSidebar ? "4" : "3"}">
<a
@@ -237,7 +242,7 @@ class PageEntry extends LitElement {
<p class="has-text-dark text">
<keyword-mark keywords="${this.query}"
>${p.url}</keyword-mark
>
>${this.thumbnailValid ? this.renderFavicon() : ""}
</p>
<p class="has-text-grey-dark text is-inline-date">
${date ? date.toLocaleString() : ""}
@@ -276,32 +281,28 @@ class PageEntry extends LitElement {
`;
}
async updateFavIcon() {
if (!this.page.favIconUrl) {
this.favIconData = null;
private renderPageIcon() {
if (!this.thumbnailValid) {
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;
}
const resp = await fetch(
`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`,
);
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;
}
return html`<img
class="favicon"
@error=${() => (this.iconValid = false)}
src=${`${this.replayPrefix}/${this.page.timestamp}id_/${this.page.favIconUrl}`}
loading="lazy"
/>`;
}
updateSnippet() {
+108 -81
View File
@@ -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<string, any>) {
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</a
>
<div class="column thumbnail">
<span class="sr-only">Page thumbnail or favicon</span>
</div>
<a
role="button"
href="#"
@click="${this.onSort}"
@keyup="${clickOnSpacebarPress}"
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
? "desc"
: "asc"
@@ -869,10 +892,10 @@ class Pages extends LitElement {
return html` <li class="page-entry ${selected ? "selected" : ""}">
<wr-page-entry
.index="${this.query || this.isSidebar ? i + 1 : 0}"
.editable="${this.editable}"
.selected="${selected}"
.isCurrent="${this.isCurrPage(p)}"
.isSidebar="${this.isSidebar}"
?editable="${this.editable}"
?selected="${selected}"
?isCurrent="${this.isCurrPage(p)}"
?isSidebar="${this.isSidebar}"
.page="${p}"
pid="${p.id}"
@sel-page="${this.onSelectToggle}"
@@ -897,7 +920,9 @@ class Pages extends LitElement {
if (!this.editable) {
return;
}
const input = this.renderRoot.querySelector("#titleEdit");
const input = this.renderRoot.querySelector(
"#titleEdit",
) as HTMLInputElement;
if (!input || !input.value.trim()) {
return;
}
@@ -1010,7 +1035,9 @@ class Pages extends LitElement {
const pageMap = {};
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) {
p.deleting = true;
pageMap[id] = p;
@@ -1099,7 +1126,7 @@ class Pages extends LitElement {
const diff =
element.scrollHeight - element.scrollTop - element.clientHeight;
if (diff < 40) {
const sorter = this.renderRoot.querySelector("wr-sorter");
const sorter = this.renderRoot.querySelector("wr-sorter") as Sorter;
if (sorter) {
sorter.getMore();
}
+1 -1
View File
@@ -1 +1 @@
// Cherry-picked Shoelace components:
// Cherry-picked Shoelace components: