From d399f8d24d56f6a140fcb57c37319792fcee985b Mon Sep 17 00:00:00 2001 From: nikitalokhmachev-ai Date: Fri, 23 May 2025 11:10:15 -0400 Subject: [PATCH 1/5] feat: working shared archives --- src/argo-archive-list.ts | 6 +- src/argo-shared-archive-list.ts | 547 ++++++++++++++++++++++++++++++ src/ext/bg.ts | 7 +- src/global-webtorrent.ts | 1 + src/localstorage.ts | 15 + src/sidepanel.ts | 166 +++++++-- src/types.ts | 15 + src/types/webtorrent-browser.d.ts | 169 ++++++++- src/types/webtorrent-global.d.ts | 49 ++- static/sidepanel.html | 3 + 10 files changed, 941 insertions(+), 37 deletions(-) create mode 100644 src/argo-shared-archive-list.ts create mode 100644 src/global-webtorrent.ts diff --git a/src/argo-archive-list.ts b/src/argo-archive-list.ts index abc60fe..7550149 100644 --- a/src/argo-archive-list.ts +++ b/src/argo-archive-list.ts @@ -7,7 +7,7 @@ import "@material/web/list/list-item.js"; import "@material/web/checkbox/checkbox.js"; import "@material/web/icon/icon.js"; import "@material/web/labs/card/elevated-card.js"; - +// @ts-expect-error import filingDrawer from "assets/images/filing-drawer.avif"; import { getLocalOption } from "./localstorage"; @@ -184,15 +184,12 @@ export class ArgoArchiveList extends LitElement { 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); }); @@ -204,6 +201,7 @@ export class ArgoArchiveList extends LitElement { this.filteredPages = [...this.pages]; } else { // partial matches on title/text via the “match” preset + // @ts-expect-error const matches = this.flex.search(this.filterQuery) as string[]; this.filteredPages = this.pages.filter((p) => matches.includes(p.ts)); } diff --git a/src/argo-shared-archive-list.ts b/src/argo-shared-archive-list.ts new file mode 100644 index 0000000..247e8ab --- /dev/null +++ b/src/argo-shared-archive-list.ts @@ -0,0 +1,547 @@ +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"; +import "@material/web/list/list-item.js"; +import "@material/web/checkbox/checkbox.js"; +import "@material/web/icon/icon.js"; +import "@material/web/labs/card/elevated-card.js"; +import "@material/web/button/filled-button.js"; +import "@material/web/button/outlined-button.js"; +// @ts-expect-error +import filingDrawer from "assets/images/filing-drawer.avif"; + +import { getLocalOption } from "./localstorage"; +import { Index as FlexIndex } from "flexsearch"; +import type { SharedArchive } from "./types"; +import { setSharedArchives } from "./localstorage"; + +import { webtorrentClient as client } from "./global-webtorrent"; + +@customElement("argo-shared-archive-list") +export class ArgoSharedArchiveList extends LitElement { + static styles: CSSResultGroup = [ + typescaleStyles as unknown as CSSResultGroup, + css` + md-elevated-card { + display: block; + margin: 1rem 0; + padding: 0; + overflow: visible; + } + .card-container { + padding: 0 1rem; + } + + .center-flex-container { + display: flex; + align-items: center; + justify-content: center; + } + + md-elevated-card > details { + border-radius: inherit; + overflow: hidden; + margin: 0; + background: transparent; + } + + md-elevated-card > details summary { + background: transparent !important; + padding: 0.75rem 1rem; + } + + md-elevated-card > details md-list { + background: transparent; + padding: 0 0rem 0rem; + } + + md-list-item { + --md-list-item-top-space: 0px; + --md-list-item-bottom-space: 0px; + + --md-list-item-leading-space: 0px; + --md-list-item-trailing-space: 12px; + + --md-list-item-one-line-container-height: 0px; + } + + .md-badge { + display: block; + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + font-size: var(--md-sys-typescale-label-small); + border-radius: 999px; + padding: 2px 6px; + } + + .leading-group { + display: flex; + gap: 0px; + align-items: center; + height: 100%; + } + + img.favicon { + width: 20px !important; + height: 20px !important; + flex: 0 0 auto; + object-fit: cover; + border-radius: 4px; + filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.6)); + } + + summary { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem; + cursor: pointer; + user-select: none; + } + summary::-webkit-details-marker { + display: none; + } + + summary md-icon.arrow-right, + summary md-icon.arrow-down { + display: none; + } + details:not([open]) summary md-icon.arrow-right { + display: block; + } + details[open] summary md-icon.arrow-down { + display: block; + } + + .title-url { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + overflow: hidden; + white-space: nowrap; + } + .title-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .base-url { + 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: var(--md-sys-color-secondary-container); + color: black; + font-weight: bold; + padding: 0 2px; + border-radius: 2px; + } + + .search-error-container { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 5rem; + + & img { + width: 100%; + max-width: 128px; + margin-bottom: 1rem; + } + + & p { + margin: 0 0 0.5rem 0; + } + } + `, + ]; + + @property({ type: Array }) + set sharedArchives(value: SharedArchive[]) { + const oldValue = this._sharedArchives; + this._sharedArchives = value; + this.requestUpdate("sharedArchives", oldValue); + } + + get sharedArchives(): SharedArchive[] { + return this._sharedArchives; + } + + private _sharedArchives: SharedArchive[] = []; + + @state() private collId = ""; + @state() private selectedPages = new Set(); + @state() private filteredPages = [] as Array<{ + id: string; + ts: string; + url: string; + title?: string; + favIconUrl?: string; + text?: string; + }>; + + @property({ type: String }) filterQuery = ""; + private flex: FlexIndex = new FlexIndex({ + tokenize: "forward", + resolution: 3, + }); + + protected updated(changed: PropertyValues) { + super.updated(changed); + + // Rebuild the index when the shared archives change: + if (changed.has("sharedArchives")) { + this.flex = new FlexIndex({ + tokenize: "forward", + resolution: 3, + }); + this.sharedArchives + .flatMap((a) => a.pages) + .forEach((p) => { + // include title + text (and URL if you like) + const toIndex = [p.title ?? "", p.text ?? ""].join(" "); + this.flex.add(p.ts, toIndex); + }); + } + + // Whenever sharedArchives or the query change, recompute filteredPages: + if (changed.has("sharedArchives") || changed.has("filterQuery")) { + if (!this.filterQuery.trim()) { + this.filteredPages = this.sharedArchives.flatMap((a) => a.pages); + } else { + // partial matches on title/text via the "match" preset + // @ts-expect-error + const matches = this.flex.search(this.filterQuery) as string[]; + this.filteredPages = this.sharedArchives + .flatMap((a) => a.pages) + .filter((p) => matches.includes(p.ts)); + } + } + } + + public clearSelection() { + this.selectedPages = new Set(); + this.requestUpdate(); + this.dispatchEvent( + new CustomEvent("selection-change", { + detail: { count: 0 }, + bubbles: true, + composed: true, + }), + ); + } + + private togglePageSelection(ts: string) { + const next = new Set(this.selectedPages); + if (next.has(ts)) { + next.delete(ts); + } else { + next.add(ts); + } + this.selectedPages = next; + this.dispatchEvent( + new CustomEvent("selection-change", { + detail: { count: this.selectedPages.size }, + bubbles: true, + composed: true, + }), + ); + } + + async connectedCallback() { + super.connectedCallback(); + console.log("Currently seeding torrents:", client.torrents); + this.collId = (await getLocalOption("defaultCollId")) || ""; + } + + 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}`) + "..."; + } + + private _copyLink(uri: string) { + navigator.clipboard.writeText(uri); + // optionally: show toast/alert + } + + private async _unseed(id: string) { + const record = this.sharedArchives.find((a) => a.id === id); + if (record) { + const torrent = client.get(record.magnetURI); + if (torrent) { + torrent.destroy(); + } + } + + // remove from storage + const all = this.sharedArchives.filter((a) => a.id !== id); + // persist back to storage + await setSharedArchives(all); + // fire an event so the parent component updates its state + this.dispatchEvent( + new CustomEvent("shared-archives-changed", { + detail: { sharedArchives: all }, + bubbles: true, + composed: true, + }), + ); + console.log("Currently sharing archives:", this.sharedArchives); + console.log("Currently seeding torrents:", client.torrents); + } + + protected render() { + // No shared archives at all + if (!this.sharedArchives || !this.sharedArchives.length) { + return html` +
+
+ +

No shared archives yet

+

+ Share some pages to see them here +

+
+
+ `; + } + + // Build a date-grouped map of SharedArchive[] + const groups = this.sharedArchives.reduce( + (acc, archive) => { + const key = this._formatDate(new Date(archive.seededAt)); + (acc[key] ||= []).push(archive); + return acc; + }, + {} as Record, + ); + + // If a filter is applied but no pages match + if (this.filterQuery && !this.filteredPages.length) { + return html` +
+
+ +

No results found

+

+ Try searching for something else +

+
+
+ `; + } + + // Render each date group + return html` +
+ ${Object.entries(groups) + .sort(([a], [b]) => new Date(b).getTime() - new Date(a).getTime()) + .map( + ([dateLabel, archives]) => html` + +
+ ${dateLabel} +
+ + ${archives.map( + (archive) => html` + +
+ + + chevron_right + expand_more + + ${archive.pages.length} + page${archive.pages.length === 1 ? "" : "s"} + + ${this.filterQuery + ? html` + + ${archive.pages.filter((p) => + this.filteredPages.some( + (fp) => fp.ts === p.ts, + ), + ).length} + + ` + : ""} + + + + + ${archive.pages + .sort((a, b) => Number(b.ts) - Number(a.ts)) + .filter((p) => + this.filterQuery + ? this.filteredPages.some((fp) => fp.ts === p.ts) + : true, + ) + .map((page) => { + const u = new URL(page.url); + return html` + this._openPage(page)} + > +
+ { + e.stopPropagation(); + this.togglePageSelection(page.ts); + }} + > + ${page.favIconUrl + ? html` + favicon of ${u.hostname} + ` + : html` + article + `} +
+
+ ${page.title || page.url} + ${u.hostname} +
+
+ + ${this.filterQuery && page.text + ? html` +
+ +
+ ` + : ""} + `; + })} +
+ + +
+ this._copyLink(archive.magnetURI)} + > + Copy Link + + this._unseed(archive.id)} + > + Unseed + +
+
+
+ `, + )} + `, + )} +
+ `; + } + + private _formatDate(date: Date): string { + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const opts: Intl.DateTimeFormatOptions = { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }; + const label = date.toLocaleDateString("en-US", opts); + if (date.toDateString() === today.toDateString()) return `Today — ${label}`; + if (date.toDateString() === yesterday.toDateString()) + return `Yesterday — ${label}`; + return label; + } + + 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}`; + + 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 631b70b..0d636b5 100644 --- a/src/ext/bg.ts +++ b/src/ext/bg.ts @@ -7,8 +7,8 @@ import { getLocalOption, removeLocalOption, setLocalOption, + getSharedArchives, } from "../localstorage"; - // =========================================================================== self.recorders = {}; self.newRecId = null; @@ -195,6 +195,11 @@ function sidepanelHandler(port) { await setLocalOption("defaultCollId", defaultCollId); break; } + case "getSharedArchives": { + const arr = await getSharedArchives(); + port.postMessage({ type: "sharedArchives", sharedArchives: arr }); + break; + } } }); diff --git a/src/global-webtorrent.ts b/src/global-webtorrent.ts new file mode 100644 index 0000000..64db176 --- /dev/null +++ b/src/global-webtorrent.ts @@ -0,0 +1 @@ +export const webtorrentClient = new window.WebTorrent(); diff --git a/src/localstorage.ts b/src/localstorage.ts index 6907a89..d3be33f 100644 --- a/src/localstorage.ts +++ b/src/localstorage.ts @@ -1,3 +1,18 @@ +import { SharedArchive } from "./types"; + +const SHARED_KEY = "sharedArchives"; + +/** Fetch the entire array (or [] if none) */ +export async function getSharedArchives(): Promise { + const { [SHARED_KEY]: arr } = await chrome.storage.local.get(SHARED_KEY); + return Array.isArray(arr) ? arr : []; +} + +/** Overwrite the stored array */ +export async function setSharedArchives(arr: SharedArchive[]): Promise { + await chrome.storage.local.set({ [SHARED_KEY]: arr }); +} + // @ts-expect-error - TS7006 - Parameter 'name' implicitly has an 'any' type. | TS7006 - Parameter 'value' implicitly has an 'any' type. export function setLocalOption(name, value) { // @ts-expect-error - TS2339 - Property 'chrome' does not exist on type 'Window & typeof globalThis'. | TS2339 - Property 'chrome' does not exist on type 'Window & typeof globalThis'. diff --git a/src/sidepanel.ts b/src/sidepanel.ts index 66cecc9..20de401 100644 --- a/src/sidepanel.ts +++ b/src/sidepanel.ts @@ -3,10 +3,14 @@ import { styles as typescaleStyles } from "@material/web/typography/md-typescale import { LitElement, html, css, CSSResultGroup } from "lit"; import { unsafeSVG } from "lit/directives/unsafe-svg.js"; import "./argo-archive-list"; +import "./argo-shared-archive-list"; import "@material/web/textfield/outlined-text-field.js"; import "@material/web/icon/icon.js"; import { ArgoArchiveList } from "./argo-archive-list"; import { Downloader } from "./sw/downloader"; +import type { SharedArchive } from "./types"; +import { getSharedArchives, setSharedArchives } from "./localstorage"; +import { webtorrentClient as client } from "./global-webtorrent"; import { getLocalOption, @@ -29,6 +33,13 @@ import { CollectionLoader } from "@webrecorder/wabac/swlib"; document.adoptedStyleSheets.push(typescaleStyles.styleSheet!); +const WS_TRACKERS = [ + "wss://tracker.openwebtorrent.com", + "wss://tracker.btorrent.xyz", + "wss://tracker.fastcast.nz", + "wss://tracker.clear.netty.link", +]; + const collLoader = new CollectionLoader(); class ArgoViewer extends LitElement { static styles: CSSResultGroup = [ @@ -101,7 +112,7 @@ class ArgoViewer extends LitElement { font-size: 12px; font-weight: 500; color: #6b6b6b; - margin-bottom: 4px; + margin-bottom: 6px; } .status-content { @@ -155,6 +166,13 @@ class ArgoViewer extends LitElement { pointer-events: auto; } + .tab-panel { + display: none; + } + .tab-panel[active] { + display: block; + } + md-icon[filled] { font-variation-settings: "FILL" 1; } @@ -164,6 +182,8 @@ class ArgoViewer extends LitElement { private archiveList!: ArgoArchiveList; constructor() { super(); + // @ts-expect-error - TS2339 - Property 'activeTabIndex' does not exist on type 'ArgoViewer'. + this.activeTabIndex = 0; // @ts-expect-error - TS2339 - Property 'selectedCount' does not exist on type 'ArgoViewer'. this.selectedCount = 0; // @ts-expect-error - TS2339 - Property 'searchQuery' does not exist on type 'ArgoViewer'. @@ -217,6 +237,9 @@ class ArgoViewer extends LitElement { this.behaviorMsg = ""; // @ts-expect-error - TS2339 - Property 'autorun' does not exist on type 'ArgoViewer'. this.autorun = false; + + // @ts-expect-error - TS2339 - Property 'sharedArchives' does not exist on type 'ArgoViewer'. + this.sharedArchives = []; } static get properties() { @@ -227,6 +250,7 @@ class ArgoViewer extends LitElement { collId: { type: String }, collTitle: { type: String }, collDrop: { type: String }, + sharedArchives: { type: Array }, recording: { type: Boolean }, status: { type: Object }, @@ -244,6 +268,7 @@ class ArgoViewer extends LitElement { behaviorResults: { type: Object }, behaviorMsg: { type: String }, autorun: { type: Boolean }, + activeTabIndex: { type: Number }, }; } @@ -360,6 +385,40 @@ class ArgoViewer extends LitElement { await this.onShare([currentPage]); } + private async reseedAll() { + const shared: SharedArchive[] = await getSharedArchives(); + if (!shared.length) return; + + const opfsRoot = await navigator.storage.getDirectory(); + console.log(`♻️ reseedAll: found ${shared.length} archives`); + + for (const record of shared) { + try { + // Skip if already seeding + if (client.get(record.magnetURI)) continue; + + // Get file handle and file from OPFS + const handle = await opfsRoot.getFileHandle(record.filename, { + create: false, + }); + const file = await handle.getFile(); + + client.seed( + file, + WS_TRACKERS.length + ? { announce: WS_TRACKERS, name: record.filename } + : undefined, + (torrent) => { + console.log(`♻️ Re-seeding ${record.filename}:`, torrent.infoHash); + console.log("Magnet:", torrent.magnetURI); + }, + ); + } catch (err) { + console.warn(`⚠️ Could not reseed ${record.filename}:`, err); + } + } + } + // @ts-expect-error - TS7006 - Parameter 'pages' implicitly has an 'any' type. private async onShare(pages) { const defaultCollId = (await getLocalOption("defaultCollId")) || ""; @@ -406,12 +465,9 @@ class ArgoViewer extends LitElement { const fileHandle = await opfsRoot.getFileHandle(filename); const file = await fileHandle.getFile(); - // Create a WebTorrent client if not already available - const client = new (window as any).WebTorrent(); - // Seed the file // @ts-expect-error - client.seed(file, (torrent) => { + client.seed(file, async (torrent) => { const magnetURI = torrent.magnetURI; console.log("Seeding WACZ file via WebTorrent:", magnetURI); @@ -425,16 +481,49 @@ class ArgoViewer extends LitElement { console.error("Failed to copy magnet link:", err); alert(`Magnet Link Ready:\n${magnetURI}`); }); + + const existing = await getSharedArchives(); + const record: SharedArchive = { + id: Date.now().toString(), + pages, + magnetURI, + filename, + seededAt: Date.now(), + }; + const updated = [record, ...existing]; + await setSharedArchives(updated); + this.sendMessage({ + type: "sharedArchives", + sharedArchives: updated, + }); + + // 3) Update reactive property for the UI + // @ts-expect-error + this.sharedArchives = updated; + // @ts-expect-error + console.log("Shared archives updated:", this.sharedArchives); + console.log("Currently seeding torrents:", client.torrents); }); } - firstUpdated() { + async firstUpdated() { this.archiveList = this.shadowRoot!.getElementById( "archive-list", ) as ArgoArchiveList; console.log("Archive list:", this.archiveList); this.registerMessages(); + + getSharedArchives().then((arr) => { + // @ts-expect-error - this.sharedArchives does not exist + this.sharedArchives = arr; + // @ts-expect-error + console.log("Shared archives:", this.sharedArchives); + }); + + await this.reseedAll(); + + console.log("Currently seeding (firstUpdated) torrents:", client.torrents); } updateTabInfo() { @@ -890,10 +979,20 @@ class ArgoViewer extends LitElement { }" > - { + // @ts-expect-error + this.activeTabIndex = 0; + }} + class="md-typescale-label-large" >My Archives - { + // @ts-expect-error + this.activeTabIndex = 1; + }} + class="md-typescale-label-large" >My Shared Archives @@ -902,19 +1001,19 @@ class ArgoViewer extends LitElement {
-
+
{ this.archiveList.clearSelection(); - //@ts-expect-error + // @ts-expect-error this.selectedCount = 0; }} > @@ -922,14 +1021,14 @@ class ArgoViewer extends LitElement { ${ - //@ts-expect-error + // @ts-expect-error this.selectedCount } selected
-
+
download @@ -951,11 +1050,18 @@ class ArgoViewer extends LitElement {
- // @ts-expect-error - (this.selectedCount = e.detail.count)} + @selection-change=${( + e: CustomEvent, // @ts-expect-error + ) => (this.selectedCount = e.detail.count)} > -
+
${this.renderStatusCard()}
-
- +
+ { + console.log("Shared archives changed event:", e.detail); + // @ts-expect-error + this.sharedArchives = e.detail.sharedArchives; + }} + >
`; diff --git a/src/types.ts b/src/types.ts index d261732..5bb5859 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,3 +20,18 @@ export type BtrixOpts = { orgName: string; client?: BtrixClient; }; + +export interface SharedArchive { + id: string; + pages: Array<{ + id: string; + ts: string; + url: string; + title?: string; + favIconUrl?: string; + text?: string; + }>; + magnetURI: string; + filename: string; + seededAt: number; +} diff --git a/src/types/webtorrent-browser.d.ts b/src/types/webtorrent-browser.d.ts index 17732ad..826f397 100644 --- a/src/types/webtorrent-browser.d.ts +++ b/src/types/webtorrent-browser.d.ts @@ -1,22 +1,169 @@ +// src/types/webtorrent-browser.d.ts + declare module "webtorrent" { - import type { TorrentOptions, Torrent, TorrentFile } from "webtorrent/types"; + import { EventEmitter } from "events"; // Node.js EventEmitter + import { Readable } from "stream"; // Node.js Readable stream + import * as MagnetUri from "magnet-uri"; // Magnet URI parser :contentReference[oaicite:9]{index=9} + import * as ParseTorrentFile from "parse-torrent-file"; // Parsed torrent file types + import type { Instance as BittorrentProtocol } from "bittorrent-protocol"; // Wire protocol types :contentReference[oaicite:11]{index=11} + + /** Main WebTorrent client */ + export default class WebTorrent extends EventEmitter { + /** Default tracker list */ + defaultAnnounceList: string[][]; - export default class WebTorrent { - constructor(options?: any); + constructor(opts?: TorrentOptions); + /** Add or load a torrent */ add( - torrentId: string, - options: TorrentOptions | undefined, - cb: (torrent: Torrent) => void, + torrentId: + | string + | Buffer + | MagnetUri.Instance + | ParseTorrentFile.Instance, + opts?: TorrentOptions, + onTorrent?: (torrent: Torrent) => void, ): Torrent; - add(torrentId: string, cb: (torrent: Torrent) => void): Torrent; - + /** Create and seed a torrent */ seed( - input: File | File[] | Blob | Blob[] | string | Buffer, - cb?: (torrent: Torrent) => void, + input: + | string + | File + | Blob + | Buffer + | Readable + | Array, + opts?: TorrentOptions, + onSeed?: (torrent: Torrent) => void, ): Torrent; - destroy(cb?: () => void): void; + /** Remove a torrent (and optionally delete data) */ + remove( + torrentId: string | Torrent, + opts?: { destroyStore?: boolean }, + cb?: (err: Error | null) => void, + ): void; + + /** Lookup an existing torrent by infoHash */ + get(infoHash: string): Torrent | undefined; + + /** Destroy client and all torrents */ + destroy(cb?: (err?: Error) => void): void; + + /** All torrents in this client */ + torrents: Torrent[]; + } + + /** Options for add() and seed() */ + export interface TorrentOptions { + announce?: string[]; // Trackers :contentReference[oaicite:12]{index=12} + getAnnounceOpts?: Function; // Custom announce params :contentReference[oaicite:13]{index=13} + urlList?: string[]; // Web seeds :contentReference[oaicite:14]{index=14} + maxWebConns?: number; // Per-seed connection limit :contentReference[oaicite:15]{index=15} + path?: string; // Download path (Node.js) :contentReference[oaicite:16]{index=16} + private?: boolean; // No DHT/PEX if true :contentReference[oaicite:17]{index=17} + maxConns?: number; // Max peers per torrent :contentReference[oaicite:18]{index=18} + dht?: boolean | object; // DHT enable/options :contentReference[oaicite:19]{index=19} + tracker?: boolean | object; // Tracker enable/options :contentReference[oaicite:20]{index=20} + lsd?: boolean; // Local discovery :contentReference[oaicite:21]{index=21} + utp?: boolean; // uTP support :contentReference[oaicite:22]{index=22} + webSeeds?: boolean; // Web seeds support :contentReference[oaicite:23]{index=23} + blocklist?: string[] | string; // Blocklist :contentReference[oaicite:24]{index=24} + downloadLimit?: number; // Throttle download (bytes/sec) :contentReference[oaicite:25]{index=25} + uploadLimit?: number; // Throttle upload (bytes/sec) :contentReference[oaicite:26]{index=26} + store?: any; // Custom storage engine :contentReference[oaicite:27]{index=27} + storeOpts?: any; // Storage engine options :contentReference[oaicite:28]{index=28} + skipVerify?: boolean; // Skip piece verification :contentReference[oaicite:29]{index=29} + strategy?: "sequential" | "rarest"; // Piece selection strategy :contentReference[oaicite:30]{index=30} + } + + /** Single file within a torrent */ + export interface TorrentFile { + name: string; // File name :contentReference[oaicite:31]{index=31} + path: string; // Path within torrent :contentReference[oaicite:32]{index=32} + length: number; // Total size in bytes :contentReference[oaicite:33]{index=33} + offset: number; // Byte offset :contentReference[oaicite:34]{index=34} + downloaded: number; // Bytes downloaded :contentReference[oaicite:35]{index=35} + progress: number; // 0 → 1 :contentReference[oaicite:36]{index=36} + + // Browser-only + getBlob(cb: (err: any, blob: Blob) => void): void; // :contentReference[oaicite:37]{index=37} + appendTo( + root: string | Element, + opts?: { + autoplay?: boolean; + muted?: boolean; + controls?: boolean; + maxBlobLength?: number; + }, + cb?: (err: any, elem: Element) => void, + ): void; // :contentReference[oaicite:38]{index=38} + renderTo( + elem: string | Element, + opts?: { + autoplay?: boolean; + muted?: boolean; + controls?: boolean; + maxBlobLength?: number; + }, + cb?: (err: any, elem: Element) => void, + ): void; // :contentReference[oaicite:39]{index=39} + + // Node.js + createReadStream(opts?: { start?: number; end?: number }): Readable; // :contentReference[oaicite:40]{index=40} + select(start?: number, end?: number): void; // :contentReference[oaicite:41]{index=41} + deselect(): void; // :contentReference[oaicite:42]{index=42} + getBuffer(cb: (err: any, buffer: Buffer) => void): void; // :contentReference[oaicite:43]{index=43} + getBlobURL(cb: (err: any, url: string) => void): void; // :contentReference[oaicite:44]{index=44} + } + + /** Torrent instance returned by add()/seed() */ + export interface Torrent extends EventEmitter { + infoHash: string; // 20-byte SHA-1 hex :contentReference[oaicite:45]{index=45} + magnetURI: string; // magnet:?xt=... :contentReference[oaicite:46]{index=46} + name: string; // Torrent name :contentReference[oaicite:47]{index=47} + announce: string[]; // Tracker list :contentReference[oaicite:48]{index=48} + urlList: string[]; // Web seeds :contentReference[oaicite:49]{index=49} + files: TorrentFile[]; // All files :contentReference[oaicite:50]{index=50} + + // Progress + timeRemaining: number; // ms left :contentReference[oaicite:51]{index=51} + progress: number; // 0 → 1 :contentReference[oaicite:52]{index=52} + downloaded: number; // Bytes :contentReference[oaicite:53]{index=53} + downloadSpeed: number; // B/s :contentReference[oaicite:54]{index=54} + uploadSpeed: number; // B/s :contentReference[oaicite:55]{index=55} + numPeers: number; // Connected peers :contentReference[oaicite:56]{index=56} + wires: BittorrentProtocol[]; // Raw wires :contentReference[oaicite:57]{index=57} + + // Control + pause(): void; // :contentReference[oaicite:58]{index=58} + resume(): void; // :contentReference[oaicite:59]{index=59} + addPeer(addr: string): void; // :contentReference[oaicite:60]{index=60} + removePeer(addr: string): void; // :contentReference[oaicite:61]{index=61} + + // Events + on(event: "infoHash", cb: () => void): this; // :contentReference[oaicite:62]{index=62} + on(event: "metadata", cb: () => void): this; // :contentReference[oaicite:63]{index=63} + on(event: "ready", cb: () => void): this; // :contentReference[oaicite:64]{index=64} + on(event: "warning", cb: (err: Error) => void): this; // :contentReference[oaicite:65]{index=65} + on(event: "error", cb: (err: Error) => void): this; // :contentReference[oaicite:66]{index=66} + on(event: "done", cb: () => void): this; // :contentReference[oaicite:67]{index=67} + on(event: "download", cb: (bytes: number) => void): this; // :contentReference[oaicite:68]{index=68} + on(event: "upload", cb: (bytes: number) => void): this; // :contentReference[oaicite:69]{index=69} + on( + event: "wire", + cb: (wire: BittorrentProtocol, addr: string) => void, + ): this; // :contentReference[oaicite:70]{index=70} + on( + event: "noPeers", + cb: (announceType: "tracker" | "dht" | "lsd") => void, + ): this; // :contentReference[oaicite:71]{index=71} + + /** Create an HTTP server for streaming (Node.js only) */ + createServer(): import("http").Server; // :contentReference[oaicite:72]{index=72} + + /** Destroy this torrent */ + destroy(cb?: (err?: Error) => void): void; // :contentReference[oaicite:73]{index=73} } } diff --git a/src/types/webtorrent-global.d.ts b/src/types/webtorrent-global.d.ts index 158aed8..dbb6f6c 100644 --- a/src/types/webtorrent-global.d.ts +++ b/src/types/webtorrent-global.d.ts @@ -6,13 +6,58 @@ interface WebTorrentFile { } interface WebTorrentTorrent { + infoHash: string; magnetURI: string; + name: string; + announce: string[]; + urlList: string[]; files: WebTorrentFile[]; + timeRemaining: number; + progress: number; + downloaded: number; + downloadSpeed: number; + uploadSpeed: number; + numPeers: number; + wires: any[]; + + pause(): void; + resume(): void; + addPeer(addr: string): void; + removePeer(addr: string): void; + + select(start?: number, end?: number): void; + deselect(): void; + + file(name: string): WebTorrentFile | undefined; + createServer(): import("http").Server; + destroy(cb?: (err?: Error) => void): void; } interface WebTorrentInstance { - add(torrentId: string, callback: (torrent: WebTorrentTorrent) => void): void; - seed(file: File | Blob, callback: (torrent: WebTorrentTorrent) => void): void; + add( + torrentId: string | Buffer, + opts?: WebTorrentOptions, + cb?: (torrent: WebTorrentTorrent) => void, + ): WebTorrentTorrent; + + add( + torrentId: string, + cb: (torrent: WebTorrentTorrent) => void, + ): WebTorrentTorrent; + seed( + input: File | Blob | string | Buffer | WebTorrentFile[], + opts?: WebTorrentOptions, + cb?: (torrent: WebTorrentTorrent) => void, + ): WebTorrentTorrent; + + remove( + torrentId: string, + opts?: { destroyStore?: boolean }, + cb?: (err: Error | null) => void, + ): void; + get(infoHash: string): WebTorrentTorrent | undefined; + destroy(cb?: () => void): void; + torrents: WebTorrentTorrent[]; } interface Window { diff --git a/static/sidepanel.html b/static/sidepanel.html index 26abcea..d704aa8 100644 --- a/static/sidepanel.html +++ b/static/sidepanel.html @@ -20,6 +20,9 @@ --md-sys-color-surface-container: white; --md-elevated-card-container-color: white; --md-icon-size: 20px; + --md-icon-button-icon-size: 20px; + --md-checkbox-container-size: 16px; + --md-checkbox-icon-size: 16px; --md-sys-color-primary: rgb(220, 101, 3); --md-sys-color-surface-tint: rgb(154 70 0); From 6095e71be357235c278bc8e08dee8b4c6fca3880 Mon Sep 17 00:00:00 2001 From: nikitalokhmachev-ai Date: Fri, 23 May 2025 13:17:54 -0400 Subject: [PATCH 2/5] feat: styling --- src/argo-archive-list.ts | 9 + src/argo-shared-archive-list.ts | 460 +++++++++++++------------------- 2 files changed, 189 insertions(+), 280 deletions(-) diff --git a/src/argo-archive-list.ts b/src/argo-archive-list.ts index 7550149..530c7da 100644 --- a/src/argo-archive-list.ts +++ b/src/argo-archive-list.ts @@ -59,6 +59,15 @@ export class ArgoArchiveList extends LitElement { --md-list-item-trailing-space: 12px; --md-list-item-one-line-container-height: 0px; + --md-list-item-hover-state-layer-opacity: 0; + } + + md-list-item[type="button"]:hover { + background: transparent !important; + } + + md-list-item md-ripple { + display: none !important; } .md-badge { diff --git a/src/argo-shared-archive-list.ts b/src/argo-shared-archive-list.ts index 247e8ab..69f03cf 100644 --- a/src/argo-shared-archive-list.ts +++ b/src/argo-shared-archive-list.ts @@ -4,7 +4,6 @@ import { styles as typescaleStyles } from "@material/web/typography/md-typescale import "@material/web/list/list.js"; import "@material/web/list/list-item.js"; -import "@material/web/checkbox/checkbox.js"; import "@material/web/icon/icon.js"; import "@material/web/labs/card/elevated-card.js"; import "@material/web/button/filled-button.js"; @@ -12,11 +11,9 @@ import "@material/web/button/outlined-button.js"; // @ts-expect-error import filingDrawer from "assets/images/filing-drawer.avif"; -import { getLocalOption } from "./localstorage"; +import { getLocalOption, setSharedArchives } from "./localstorage"; import { Index as FlexIndex } from "flexsearch"; import type { SharedArchive } from "./types"; -import { setSharedArchives } from "./localstorage"; - import { webtorrentClient as client } from "./global-webtorrent"; @customElement("argo-shared-archive-list") @@ -50,6 +47,14 @@ export class ArgoSharedArchiveList extends LitElement { md-elevated-card > details summary { background: transparent !important; padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + user-select: none; + } + md-elevated-card > details summary::-webkit-details-marker { + display: none; } md-elevated-card > details md-list { @@ -57,14 +62,15 @@ export class ArgoSharedArchiveList extends LitElement { padding: 0 0rem 0rem; } - md-list-item { - --md-list-item-top-space: 0px; - --md-list-item-bottom-space: 0px; - - --md-list-item-leading-space: 0px; - --md-list-item-trailing-space: 12px; - - --md-list-item-one-line-container-height: 0px; + summary md-icon.arrow-right, + summary md-icon.arrow-down { + display: none; + } + details:not([open]) summary md-icon.arrow-right { + display: block; + } + details[open] summary md-icon.arrow-down { + display: block; } .md-badge { @@ -92,29 +98,6 @@ export class ArgoSharedArchiveList extends LitElement { filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.6)); } - summary { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - cursor: pointer; - user-select: none; - } - summary::-webkit-details-marker { - display: none; - } - - summary md-icon.arrow-right, - summary md-icon.arrow-down { - display: none; - } - details:not([open]) summary md-icon.arrow-right { - display: block; - } - details[open] summary md-icon.arrow-down { - display: block; - } - .title-url { display: flex; align-items: center; @@ -133,6 +116,28 @@ export class ArgoSharedArchiveList extends LitElement { flex-shrink: 0; text-decoration: none; } + + md-list-item { + --md-list-item-top-space: 0px; + --md-list-item-bottom-space: 0px; + + --md-list-item-leading-space: 0px; + --md-list-item-trailing-space: 0px; + + --md-list-item-one-line-container-height: 0px; + padding: 0.75rem 1rem; + + --md-list-item-hover-state-layer-opacity: 0; + } + + md-list-item[type="button"]:hover { + background: transparent !important; + } + + md-list-item md-ripple { + display: none !important; + } + .search-result-text { width: 100%; padding-left: 14px; @@ -170,28 +175,17 @@ export class ArgoSharedArchiveList extends LitElement { ]; @property({ type: Array }) - set sharedArchives(value: SharedArchive[]) { - const oldValue = this._sharedArchives; - this._sharedArchives = value; - this.requestUpdate("sharedArchives", oldValue); - } - - get sharedArchives(): SharedArchive[] { - return this._sharedArchives; - } - - private _sharedArchives: SharedArchive[] = []; + sharedArchives: SharedArchive[] = []; @state() private collId = ""; - @state() private selectedPages = new Set(); - @state() private filteredPages = [] as Array<{ + @state() private filteredPages: Array<{ id: string; ts: string; url: string; title?: string; favIconUrl?: string; text?: string; - }>; + }> = []; @property({ type: String }) filterQuery = ""; private flex: FlexIndex = new FlexIndex({ @@ -202,7 +196,6 @@ export class ArgoSharedArchiveList extends LitElement { protected updated(changed: PropertyValues) { super.updated(changed); - // Rebuild the index when the shared archives change: if (changed.has("sharedArchives")) { this.flex = new FlexIndex({ tokenize: "forward", @@ -211,62 +204,45 @@ export class ArgoSharedArchiveList extends LitElement { this.sharedArchives .flatMap((a) => a.pages) .forEach((p) => { - // include title + text (and URL if you like) const toIndex = [p.title ?? "", p.text ?? ""].join(" "); this.flex.add(p.ts, toIndex); }); } - // Whenever sharedArchives or the query change, recompute filteredPages: if (changed.has("sharedArchives") || changed.has("filterQuery")) { + const allPages = this.sharedArchives.flatMap((a) => a.pages); if (!this.filterQuery.trim()) { - this.filteredPages = this.sharedArchives.flatMap((a) => a.pages); + this.filteredPages = allPages; } else { - // partial matches on title/text via the "match" preset // @ts-expect-error const matches = this.flex.search(this.filterQuery) as string[]; - this.filteredPages = this.sharedArchives - .flatMap((a) => a.pages) - .filter((p) => matches.includes(p.ts)); + this.filteredPages = allPages.filter((p) => matches.includes(p.ts)); } } } - public clearSelection() { - this.selectedPages = new Set(); - this.requestUpdate(); - this.dispatchEvent( - new CustomEvent("selection-change", { - detail: { count: 0 }, - bubbles: true, - composed: true, - }), - ); - } - - private togglePageSelection(ts: string) { - const next = new Set(this.selectedPages); - if (next.has(ts)) { - next.delete(ts); - } else { - next.add(ts); - } - this.selectedPages = next; - this.dispatchEvent( - new CustomEvent("selection-change", { - detail: { count: this.selectedPages.size }, - bubbles: true, - composed: true, - }), - ); - } - async connectedCallback() { super.connectedCallback(); - console.log("Currently seeding torrents:", client.torrents); this.collId = (await getLocalOption("defaultCollId")) || ""; } + private _formatDate(date: Date): string { + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const opts: Intl.DateTimeFormatOptions = { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + }; + const label = date.toLocaleDateString("en-US", opts); + if (date.toDateString() === today.toDateString()) return `Today — ${label}`; + if (date.toDateString() === yesterday.toDateString()) + return `Yesterday — ${label}`; + return label; + } + private _highlightMatch( text?: string, query: string = "", @@ -288,23 +264,16 @@ export class ArgoSharedArchiveList extends LitElement { private _copyLink(uri: string) { navigator.clipboard.writeText(uri); - // optionally: show toast/alert } private async _unseed(id: string) { const record = this.sharedArchives.find((a) => a.id === id); - if (record) { - const torrent = client.get(record.magnetURI); - if (torrent) { - torrent.destroy(); - } - } + if (!record) return; + const torrent = client.get(record.magnetURI); + if (torrent) torrent.destroy(); - // remove from storage const all = this.sharedArchives.filter((a) => a.id !== id); - // persist back to storage await setSharedArchives(all); - // fire an event so the parent component updates its state this.dispatchEvent( new CustomEvent("shared-archives-changed", { detail: { sharedArchives: all }, @@ -312,13 +281,31 @@ export class ArgoSharedArchiveList extends LitElement { composed: true, }), ); - console.log("Currently sharing archives:", this.sharedArchives); - console.log("Currently seeding torrents:", client.torrents); } - protected render() { - // No shared archives at all - if (!this.sharedArchives || !this.sharedArchives.length) { + 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}`; + + const extensionUrlPrefix = chrome.runtime.getURL("index.html"); + const tabs = await chrome.tabs.query({}); + + // @ts-expect-error + const viewerTab = tabs.find((t) => t.url?.startsWith(extensionUrlPrefix)); + if (viewerTab && viewerTab.id) { + chrome.tabs.update(viewerTab.id, { url: fullUrl, active: true }); + } else { + chrome.tabs.create({ url: fullUrl }); + } + } + + render() { + if (!this.sharedArchives.length) { return html`
@@ -332,7 +319,6 @@ export class ArgoSharedArchiveList extends LitElement { `; } - // Build a date-grouped map of SharedArchive[] const groups = this.sharedArchives.reduce( (acc, archive) => { const key = this._formatDate(new Date(archive.seededAt)); @@ -342,7 +328,6 @@ export class ArgoSharedArchiveList extends LitElement { {} as Record, ); - // If a filter is applied but no pages match if (this.filterQuery && !this.filteredPages.length) { return html`
@@ -357,191 +342,106 @@ export class ArgoSharedArchiveList extends LitElement { `; } - // Render each date group return html`
${Object.entries(groups) .sort(([a], [b]) => new Date(b).getTime() - new Date(a).getTime()) - .map( - ([dateLabel, archives]) => html` - -
- ${dateLabel} -
- - ${archives.map( - (archive) => html` - -
- - - chevron_right - expand_more - - ${archive.pages.length} - page${archive.pages.length === 1 ? "" : "s"} - - ${this.filterQuery - ? html` - - ${archive.pages.filter((p) => - this.filteredPages.some( - (fp) => fp.ts === p.ts, - ), - ).length} - - ` - : ""} - - - - - ${archive.pages - .sort((a, b) => Number(b.ts) - Number(a.ts)) - .filter((p) => - this.filterQuery - ? this.filteredPages.some((fp) => fp.ts === p.ts) - : true, - ) - .map((page) => { - const u = new URL(page.url); - return html` - this._openPage(page)} - > -
- { - e.stopPropagation(); - this.togglePageSelection(page.ts); - }} - > - ${page.favIconUrl - ? html` - favicon of ${u.hostname} - ` - : html` - article - `} -
-
- ${page.title || page.url} - ${u.hostname} + archives.map( + (archive) => html` + +
+ + chevron_right + expand_more + ${dateLabel} + + + + ${archive.pages + .sort((a, b) => Number(b.ts) - Number(a.ts)) + .filter((p) => + this.filterQuery + ? this.filteredPages.some((fp) => fp.ts === p.ts) + : true, + ) + .map((page) => { + const u = new URL(page.url); + return html` + this._openPage(page)} + > +
+ ${page.favIconUrl + ? html` + favicon of ${u.hostname} + ` + : html` + article + `} +
+
+ ${page.title || page.url} + ${u.hostname} +
+
+ + ${this.filterQuery && page.text + ? html` +
-
- - - ${this.filterQuery && page.text - ? html` -
- -
- ` - : ""} - `; - })} -
- - -
+
+ ` + : ""} + `; + })} + + +
+ this._copyLink(archive.magnetURI)} > - this._copyLink(archive.magnetURI)} + content_copy - Copy Link - - this._unseed(archive.id)} - > - Unseed - -
-
-
- `, - )} - `, + Copy Link + + this._unseed(archive.id)} + aria-label="Unshare" + > + share_off + +
+
+
+ `, + ), )}
`; } - - private _formatDate(date: Date): string { - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); - const opts: Intl.DateTimeFormatOptions = { - weekday: "long", - month: "long", - day: "numeric", - year: "numeric", - }; - const label = date.toLocaleDateString("en-US", opts); - if (date.toDateString() === today.toDateString()) return `Today — ${label}`; - if (date.toDateString() === yesterday.toDateString()) - return `Yesterday — ${label}`; - return label; - } - - 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}`; - - 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 }); - } - } } From df5c9b4dcd5d317a2e19542ecc4463944a8302f8 Mon Sep 17 00:00:00 2001 From: nikitalokhmachev-ai Date: Fri, 23 May 2025 14:33:00 -0400 Subject: [PATCH 3/5] feat: no need for a check --- src/sidepanel.ts | 80 +++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/src/sidepanel.ts b/src/sidepanel.ts index 20de401..d33d554 100644 --- a/src/sidepanel.ts +++ b/src/sidepanel.ts @@ -394,9 +394,6 @@ class ArgoViewer extends LitElement { for (const record of shared) { try { - // Skip if already seeding - if (client.get(record.magnetURI)) continue; - // Get file handle and file from OPFS const handle = await opfsRoot.getFileHandle(record.filename, { create: false, @@ -466,44 +463,49 @@ class ArgoViewer extends LitElement { const file = await fileHandle.getFile(); // Seed the file - // @ts-expect-error - client.seed(file, async (torrent) => { - const magnetURI = torrent.magnetURI; - console.log("Seeding WACZ file via WebTorrent:", magnetURI); - - // Copy to clipboard - navigator.clipboard - .writeText(magnetURI) - .then(() => { - alert(`Magnet link copied to clipboard:\n${magnetURI}`); - }) - .catch((err) => { - console.error("Failed to copy magnet link:", err); - alert(`Magnet Link Ready:\n${magnetURI}`); + client.seed( + file, + WS_TRACKERS.length + ? { announce: WS_TRACKERS, name: filename } + : undefined, + async (torrent) => { + const magnetURI = torrent.magnetURI; + console.log("Seeding WACZ file via WebTorrent:", magnetURI); + + // Copy to clipboard + navigator.clipboard + .writeText(magnetURI) + .then(() => { + alert(`Magnet link copied to clipboard:\n${magnetURI}`); + }) + .catch((err) => { + console.error("Failed to copy magnet link:", err); + alert(`Magnet Link Ready:\n${magnetURI}`); + }); + + const existing = await getSharedArchives(); + const record: SharedArchive = { + id: Date.now().toString(), + pages, + magnetURI, + filename, + seededAt: Date.now(), + }; + const updated = [record, ...existing]; + await setSharedArchives(updated); + this.sendMessage({ + type: "sharedArchives", + sharedArchives: updated, }); - const existing = await getSharedArchives(); - const record: SharedArchive = { - id: Date.now().toString(), - pages, - magnetURI, - filename, - seededAt: Date.now(), - }; - const updated = [record, ...existing]; - await setSharedArchives(updated); - this.sendMessage({ - type: "sharedArchives", - sharedArchives: updated, - }); - - // 3) Update reactive property for the UI - // @ts-expect-error - this.sharedArchives = updated; - // @ts-expect-error - console.log("Shared archives updated:", this.sharedArchives); - console.log("Currently seeding torrents:", client.torrents); - }); + // 3) Update reactive property for the UI + // @ts-expect-error + this.sharedArchives = updated; + // @ts-expect-error + console.log("Shared archives updated:", this.sharedArchives); + console.log("Currently seeding torrents:", client.torrents); + }, + ); } async firstUpdated() { From a0756307813e591703bc94e623fbfac9de3b89e3 Mon Sep 17 00:00:00 2001 From: Henry Wilkinson Date: Sat, 24 May 2025 01:55:01 -0400 Subject: [PATCH 4/5] Deselect archives on share completion --- src/sidepanel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sidepanel.ts b/src/sidepanel.ts index d33d554..2795982 100644 --- a/src/sidepanel.ts +++ b/src/sidepanel.ts @@ -373,6 +373,9 @@ class ArgoViewer extends LitElement { } console.log("Selected pages to share:", selectedPages); await this.onShare(selectedPages); + this.archiveList.clearSelection(); + // @ts-expect-error + this.selectedCount = 0; } private async onShareCurrent() { From 550309c392a19a8f8005d98f960c4745765f8dde Mon Sep 17 00:00:00 2001 From: nikitalokhmachev-ai Date: Mon, 26 May 2025 08:50:33 -0400 Subject: [PATCH 5/5] feat: destroy torrent properly --- src/argo-shared-archive-list.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/argo-shared-archive-list.ts b/src/argo-shared-archive-list.ts index 69f03cf..d205cae 100644 --- a/src/argo-shared-archive-list.ts +++ b/src/argo-shared-archive-list.ts @@ -270,7 +270,9 @@ export class ArgoSharedArchiveList extends LitElement { const record = this.sharedArchives.find((a) => a.id === id); if (!record) return; const torrent = client.get(record.magnetURI); - if (torrent) torrent.destroy(); + if (torrent) { + await new Promise((resolve) => torrent.destroy(() => resolve())); + } const all = this.sharedArchives.filter((a) => a.id !== id); await setSharedArchives(all);