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:
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
// Cherry-picked Shoelace components:
|
||||
// Cherry-picked Shoelace components:
|
||||
|
||||
Reference in New Issue
Block a user