diff --git a/src/argo-archive-list.ts b/src/argo-archive-list.ts index abc60fe..530c7da 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"; @@ -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 { @@ -184,15 +193,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 +210,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..d205cae --- /dev/null +++ b/src/argo-shared-archive-list.ts @@ -0,0 +1,449 @@ +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/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, setSharedArchives } from "./localstorage"; +import { Index as FlexIndex } from "flexsearch"; +import type { SharedArchive } from "./types"; +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; + 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 { + background: transparent; + padding: 0 0rem 0rem; + } + + 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 { + 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)); + } + + .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; + } + + 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; + 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 }) + sharedArchives: SharedArchive[] = []; + + @state() private collId = ""; + @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({ + tokenize: "forward", + resolution: 3, + }); + + protected updated(changed: PropertyValues) { + super.updated(changed); + + if (changed.has("sharedArchives")) { + this.flex = new FlexIndex({ + tokenize: "forward", + resolution: 3, + }); + this.sharedArchives + .flatMap((a) => a.pages) + .forEach((p) => { + const toIndex = [p.title ?? "", p.text ?? ""].join(" "); + this.flex.add(p.ts, toIndex); + }); + } + + if (changed.has("sharedArchives") || changed.has("filterQuery")) { + const allPages = this.sharedArchives.flatMap((a) => a.pages); + if (!this.filterQuery.trim()) { + this.filteredPages = allPages; + } else { + // @ts-expect-error + const matches = this.flex.search(this.filterQuery) as string[]; + this.filteredPages = allPages.filter((p) => matches.includes(p.ts)); + } + } + } + + async connectedCallback() { + super.connectedCallback(); + 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 = "", + 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); + } + + private async _unseed(id: string) { + const record = this.sharedArchives.find((a) => a.id === id); + if (!record) return; + const torrent = client.get(record.magnetURI); + if (torrent) { + await new Promise((resolve) => torrent.destroy(() => resolve())); + } + + const all = this.sharedArchives.filter((a) => a.id !== id); + await setSharedArchives(all); + this.dispatchEvent( + new CustomEvent("shared-archives-changed", { + detail: { sharedArchives: all }, + bubbles: true, + composed: true, + }), + ); + } + + 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` +
+
+ +

No shared archives yet

+

+ Share some pages to see them here +

+
+
+ `; + } + + const groups = this.sharedArchives.reduce( + (acc, archive) => { + const key = this._formatDate(new Date(archive.seededAt)); + (acc[key] ||= []).push(archive); + return acc; + }, + {} as Record, + ); + + if (this.filterQuery && !this.filteredPages.length) { + return html` +
+
+ +

No results found

+

+ Try searching for something else +

+
+
+ `; + } + + return html` +
+ ${Object.entries(groups) + .sort(([a], [b]) => new Date(b).getTime() - new Date(a).getTime()) + .flatMap(([dateLabel, archives]) => + 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._copyLink(archive.magnetURI)} + > + content_copy + Copy Link + + this._unseed(archive.id)} + aria-label="Unshare" + > + share_off + +
+
+
+ `, + ), + )} +
+ `; + } +} 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..2795982 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 }, }; } @@ -348,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() { @@ -360,6 +388,37 @@ 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 { + // 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,35 +465,70 @@ 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) => { - 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, }); - }); + + // 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 +984,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 +1006,19 @@ class ArgoViewer extends LitElement {
-
+
{ this.archiveList.clearSelection(); - //@ts-expect-error + // @ts-expect-error this.selectedCount = 0; }} > @@ -922,14 +1026,14 @@ class ArgoViewer extends LitElement { ${ - //@ts-expect-error + // @ts-expect-error this.selectedCount } selected
-
+
download @@ -951,11 +1055,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);