diff --git a/package.json b/package.json index ebc898a..c80b57a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "buffer": "^6.0.3", "bulma": "^0.9.3", "client-zip": "^2.3.0", + "flexsearch": "0.7.31", "hash-wasm": "^4.9.0", "http-status-codes": "^2.1.4", "idb": "^7.1.1", diff --git a/src/argo-archive-list.ts b/src/argo-archive-list.ts index 09737c5..5fe877d 100644 --- a/src/argo-archive-list.ts +++ b/src/argo-archive-list.ts @@ -1,5 +1,5 @@ -import { LitElement, html, css, CSSResultGroup } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { LitElement, html, css, CSSResultGroup, PropertyValues } from "lit"; +import { customElement, state, property } from "lit/decorators.js"; import { styles as typescaleStyles } from "@material/web/typography/md-typescale-styles.js"; import "@material/web/list/list.js"; @@ -9,6 +9,7 @@ import "@material/web/icon/icon.js"; import "@material/web/labs/card/elevated-card.js"; import { getLocalOption } from "./localstorage"; +import { Index as FlexIndex } from "flexsearch"; @customElement("argo-archive-list") export class ArgoArchiveList extends LitElement { @@ -109,6 +110,22 @@ export class ArgoArchiveList extends LitElement { flex-shrink: 0; text-decoration: none; } + .search-result-text { + width: 100%; + padding-left: 14px; + padding-right: 12px; + padding-top: 4px; + padding-bottom: 12px; + box-sizing: border-box; + } + + .search-result-text b { + background-color: #cf7df1; + color: black; + font-weight: bold; + padding: 0 2px; + border-radius: 2px; + } `, ]; @@ -118,9 +135,54 @@ export class ArgoArchiveList extends LitElement { url: string; title?: string; favIconUrl?: string; + text?: string; }> = []; @state() private collId = ""; @state() private selectedPages = new Set(); + @state() private filteredPages = [] as typeof this.pages; + + @property({ type: String }) filterQuery = ""; + private flex: FlexIndex = new FlexIndex({ + tokenize: "forward", + resolution: 3, + }); + + protected updated(changed: PropertyValues) { + super.updated(changed); + + // 2) Rebuild the index when the raw pages change: + if (changed.has("pages")) { + this.flex = new FlexIndex({ + tokenize: "forward", + resolution: 3, + }); + this.pages.forEach((p) => { + // include title + text (and URL if you like) + + const toIndex = [p.title ?? "", p.text ?? ""].join(" "); + this.flex.add(p.ts, toIndex); + }); + } + + // 3) Whenever pages or the query change, recompute filteredPages: + if (changed.has("pages") || changed.has("filterQuery")) { + if (!this.filterQuery.trim()) { + this.filteredPages = [...this.pages]; + } else { + // partial matches on title/text via the “match” preset + const matches = this.flex.search(this.filterQuery) as string[]; + this.filteredPages = this.pages.filter((p) => matches.includes(p.ts)); + } + } + } + + private buildIndex() { + this.flex = new FlexIndex(); + this.pages.forEach((p) => { + const text = p.url + (p.title ? ` ${p.title}` : ""); + this.flex.add(p.ts, text); // use ts (timestamp) as a unique id + }); + } private togglePageSelection(ts: string) { const next = new Set(this.selectedPages); @@ -155,18 +217,37 @@ export class ArgoArchiveList extends LitElement { }); } + private _highlightMatch( + text?: string, + query: string = "", + maxLen = 180, + ): string { + if (!text) return ""; + + const safeQuery = query.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const regex = new RegExp(safeQuery, "ig"); + + const matchIndex = text.search(regex); + if (matchIndex === -1) return text.slice(0, maxLen) + "..."; + + const previewStart = Math.max(0, matchIndex - 30); + const preview = text.slice(previewStart, previewStart + maxLen); + + return preview.replace(regex, (m) => `${m}`) + "..."; + } + render() { if (!this.pages.length) { return html`

No archives yet.

`; } - const groups = this.pages.reduce( + const groups = this.filteredPages.reduce( (acc, page) => { const key = this._formatDate(new Date(Number(page.ts))); (acc[key] ||= []).push(page); return acc; }, - {} as Record, + {} as Record, ); return html` @@ -226,6 +307,20 @@ export class ArgoArchiveList extends LitElement { > + ${this.filterQuery && page.text + ? html` +
+ +
+ ` + : ""} `; })} @@ -254,15 +349,28 @@ export class ArgoArchiveList extends LitElement { return label; } - private _openPage(page: { ts: string; url: string }) { + private async _openPage(page: { ts: string; url: string }) { const tsParam = new Date(Number(page.ts)) .toISOString() .replace(/[-:TZ.]/g, ""); const urlEnc = encodeURIComponent(page.url); const fullUrl = - `${chrome.runtime.getURL("index.html")}?source=local://${ - this.collId - }&url=${urlEnc}` + `#view=pages&url=${urlEnc}&ts=${tsParam}`; - chrome.tabs.create({ url: fullUrl }); + `${chrome.runtime.getURL("index.html")}?source=local://${this.collId}` + + `&url=${urlEnc}#view=pages&url=${urlEnc}&ts=${tsParam}`; + + const extensionUrlPrefix = chrome.runtime.getURL("index.html"); + + // Check if any existing tab already displays the archive viewer + const tabs = await chrome.tabs.query({}); + // @ts-expect-error - t implicitly has an 'any' type + const viewerTab = tabs.find((t) => t.url?.startsWith(extensionUrlPrefix)); + + if (viewerTab && viewerTab.id) { + // Reuse the existing tab + chrome.tabs.update(viewerTab.id, { url: fullUrl, active: true }); + } else { + // Fallback: open a new tab + chrome.tabs.create({ url: fullUrl }); + } } } diff --git a/src/ext/bg.ts b/src/ext/bg.ts index 3e2ade9..da9e112 100644 --- a/src/ext/bg.ts +++ b/src/ext/bg.ts @@ -115,17 +115,22 @@ function sidepanelHandler(port) { defaultCollId = message.collId; autorun = message.autorun; - // @ts-expect-error - tabs doesn't have type definitions chrome.tabs.query( { active: true, currentWindow: true }, + //@ts-expect-error tabs has any type async (tabs) => { for (const tab of tabs) { if (!isValidUrl(tab.url)) continue; - // @ts-expect-error - TS2554 - Expected 2 arguments, but got 3. await startRecorder( tab.id, - { collId: defaultCollId, port: null, autorun }, + { + // @ts-expect-error - collId implicitly has an 'any' type. + collId: defaultCollId, + port: null, + autorun, + }, + //@ts-expect-error - 2 parameters but 3 tab.url, ); } @@ -232,10 +237,16 @@ chrome.tabs.onActivated.addListener(async ({ tabId }) => { if (!isValidUrl(tab.url)) return; if (!self.recorders[tabId]) { - // @ts-expect-error - TS2554 - Expected 2 arguments, but got 3. await startRecorder( tabId, - { collId: defaultCollId, port: null, autorun }, + { + // @ts-expect-error - collId implicitly has an 'any' type. + collId: defaultCollId, + port: null, + autorun, + }, + + // @ts-expect-error - 2 parameters but 3 tab.url, ); } diff --git a/src/sidepanel.ts b/src/sidepanel.ts index a0d606d..acc8769 100644 --- a/src/sidepanel.ts +++ b/src/sidepanel.ts @@ -28,7 +28,6 @@ import "@material/web/button/outlined-button.js"; import "@material/web/divider/divider.js"; import { mapIntegerToRange, truncateString } from "./utils"; import { CollectionLoader } from "@webrecorder/wabac/swlib"; -import WebTorrent from "webtorrent"; document.adoptedStyleSheets.push(typescaleStyles.styleSheet!); @@ -129,7 +128,8 @@ class ArgoViewer extends LitElement { private archiveList!: ArgoArchiveList; constructor() { super(); - + // @ts-expect-error - TS2339 - Property 'searchQuery' does not exist on type 'ArgoViewer'. + this.searchQuery = ""; // @ts-expect-error - TS2339 - Property 'collections' does not exist on type 'ArgoViewer'. this.collections = []; // @ts-expect-error - TS2339 - Property 'collTitle' does not exist on type 'ArgoViewer'. @@ -183,6 +183,7 @@ class ArgoViewer extends LitElement { static get properties() { return { + searchQuery: { type: String }, collections: { type: Array }, collId: { type: String }, collTitle: { type: String }, @@ -721,6 +722,12 @@ class ArgoViewer extends LitElement {

${this.notRecordingMessage}

`; } + private onSearchInput(e: InputEvent) { + const input = e.currentTarget as HTMLInputElement; + // @ts-expect-error - TS2339 - Property 'searchQuery' does not exist on type 'ArgoViewer'. + this.searchQuery = input.value; + } + renderSearch() { return html`
@@ -729,6 +736,11 @@ class ArgoViewer extends LitElement { placeholder="Search archived pages" aria-label="Search archived pages" class="search-field" + @input=${this.onSearchInput} + .value=${ + // @ts-expect-error - TS2339 - Property 'searchQuery' does not exist on type 'ArgoViewer'. + this.searchQuery + } > search @@ -752,7 +764,13 @@ class ArgoViewer extends LitElement { style="flex: 1; overflow-y: auto; position: relative; flex-grow: 1;" >
- +
diff --git a/yarn.lock b/yarn.lock index e901dbd..2e404c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4841,6 +4841,11 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +flexsearch@0.7.31: + version "0.7.31" + resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.31.tgz#065d4110b95083110b9b6c762a71a77cc52e4702" + integrity sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA== + flexsearch@^0.7.31: version "0.7.43" resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c"