Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,38 @@
"author": "Webrecorder Software",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@material/web": "^2.3.0",
"@fortawesome/fontawesome-free": "^5.13.0",
"@ipld/car": "^5.3.2",
"@ipld/unixfs": "^3.0.0",
"@material/web": "^2.3.0",
"@webrecorder/wabac": "^2.22.16",
"auto-js-ipfs": "^2.3.0",
"browsertrix-behaviors": "^0.8.5",
"btoa": "^1.2.1",
"buffer": "^6.0.3",
"bulma": "^0.9.3",
"client-zip": "^2.3.0",
"idb": "^7.1.1",
"hash-wasm": "^4.9.0",
"http-status-codes": "^2.1.4",
"idb": "^7.1.1",
"keyword-mark-element": "^0.1.2",
"node-fetch": "2.6.7",
"p-queue": "^8.0.1",
"pdfjs-dist": "2.2.228",
"pretty-bytes": "^5.6.0",
"process": "^0.11.10",
"replaywebpage": "^2.3.7",
"stream-browserify": "^3.0.0",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"unused-filename": "^4.0.1",
"uuid": "^9.0.0",
"warcio": "^2.4.4"
"warcio": "^2.4.4",
"webtorrent": "^2.6.3"
},
"devDependencies": {
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@types/uuid": "^10.0.0",
"copy-webpack-plugin": "^9.0.1",
"css-loader": "^6.2.0",
"electron": "^32.2.0",
Expand Down
52 changes: 44 additions & 8 deletions src/argo-archive-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,29 @@ export class ArgoArchiveList extends LitElement {
`,
];

@state() private pages: Array<{ ts: string; url: string; title?: string; favIconUrl?: string }> = [];
@state() private pages: Array<{
id: string;
ts: string;
url: string;
title?: string;
favIconUrl?: string;
}> = [];
@state() private collId = "";
@state() private selectedPages = new Set<string>();

private togglePageSelection(ts: string) {
const next = new Set(this.selectedPages);
if (next.has(ts)) {
next.delete(ts);
} else {
next.add(ts);
}
this.selectedPages = next;
}

public getSelectedPages() {
return this.pages.filter((p) => this.selectedPages.has(p.ts));
}

async connectedCallback() {
super.connectedCallback();
Expand Down Expand Up @@ -168,12 +189,18 @@ export class ArgoArchiveList extends LitElement {
.map((page) => {
const u = new URL(page.url);
return html`
<md-list-item type="button" @click=${() => this._openPage(page)}>
<md-list-item
type="button"
@click=${() => this._openPage(page)}
>
<div slot="start" class="leading-group">
<md-checkbox
slot="start"
touch-target="wrapper"
@click=${(e: Event) => e.stopPropagation()}
@click=${(e: Event) => {
e.stopPropagation();
this.togglePageSelection(page.ts);
}}
></md-checkbox>

${page.favIconUrl
Expand Down Expand Up @@ -215,19 +242,28 @@ export class ArgoArchiveList extends LitElement {
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 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}`;
if (date.toDateString() === yesterday.toDateString())
return `Yesterday — ${label}`;
return label;
}

private _openPage(page: { ts: string; url: string }) {
const tsParam = new Date(Number(page.ts)).toISOString().replace(/[-:TZ.]/g, "");
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.runtime.getURL("index.html")}?source=local://${
this.collId
}&url=${urlEnc}` + `#view=pages&url=${urlEnc}&ts=${tsParam}`;
chrome.tabs.create({ url: fullUrl });
}
}
152 changes: 150 additions & 2 deletions src/sidepanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { unsafeSVG } from "lit/directives/unsafe-svg.js";
import "./argo-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 {
getLocalOption,
Expand All @@ -22,10 +24,14 @@ import {
import "@material/web/button/filled-button.js";
import "@material/web/button/outlined-button.js";
import "@material/web/divider/divider.js";
import { CollectionLoader } from "@webrecorder/wabac/swlib";
import WebTorrent from "webtorrent";

document.adoptedStyleSheets.push(typescaleStyles.styleSheet!);

const collLoader = new CollectionLoader();
class ArgoViewer extends LitElement {
private archiveList!: ArgoArchiveList;
constructor() {
super();

Expand Down Expand Up @@ -101,7 +107,136 @@ class ArgoViewer extends LitElement {
};
}

private async onDownload() {
const selectedPages = this.archiveList?.getSelectedPages?.() || [];
if (!selectedPages.length) {
alert("Please select some pages to share.");
return;
}

console.log("Selected pages to share:", selectedPages);

const defaultCollId = (await getLocalOption("defaultCollId")) || "";
const coll = await collLoader.loadColl(defaultCollId);

const pageTsList = selectedPages.map((p) => p.id);
const format = "wacz";
const filename = `archive-${Date.now()}.wacz`;

// Webrecorder swlib API format for download:
const downloader = new Downloader({
coll,
format,
filename,
pageList: pageTsList,
});

const response = await downloader.download();
if (!(response instanceof Response)) {
console.error("Download failed:", response);
alert("Failed to download archive.");
return;
}

console.log("Download response:", response);

const blob = await response.blob();
const url = URL.createObjectURL(blob);

// Create temporary <a> to trigger download
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();

// Cleanup
URL.revokeObjectURL(url);
document.body.removeChild(a);

console.log("WACZ file downloaded:", filename);
}

private async onShare() {
const selectedPages = this.archiveList?.getSelectedPages?.() || [];
if (!selectedPages.length) {
alert("Please select some pages to share.");
return;
}

console.log("Selected pages to share:", selectedPages);

const defaultCollId = (await getLocalOption("defaultCollId")) || "";
const coll = await collLoader.loadColl(defaultCollId);

const pageTsList = selectedPages.map((p) => p.id);
const format = "wacz";
const filename = `archive-${Date.now()}.wacz`;

// Webrecorder swlib API format for download:
const downloader = new Downloader({
coll,
format,
filename,
pageList: pageTsList,
});

const response = await downloader.download();
if (!(response instanceof Response)) {
console.error("Download failed:", response);
alert("Failed to download archive.");
return;
}

const opfsRoot = await navigator.storage.getDirectory();
const waczFileHandle = await opfsRoot.getFileHandle(filename, {
create: true,
});
const writable = await waczFileHandle.createWritable();

const reader = response.body!.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
await writable.write(value);
}

await writable.close();
Comment on lines +184 to +204

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The code doesn't handle errors during file writing operations. If any error occurs during reading from the response or writing to OPFS, the writable stream might remain open, causing resource leaks. Add proper error handling with try/catch and ensure the writable is closed even if an error occurs. [possible issue, importance: 7]

Suggested change
const response = await downloader.download();
if (!(response instanceof Response)) {
console.error("Download failed:", response);
alert("Failed to download archive.");
return;
}
const opfsRoot = await navigator.storage.getDirectory();
const waczFileHandle = await opfsRoot.getFileHandle(filename, {
create: true,
});
const writable = await waczFileHandle.createWritable();
const reader = response.body!.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
await writable.write(value);
}
await writable.close();
const response = await downloader.download();
if (!(response instanceof Response)) {
console.error("Download failed:", response);
alert("Failed to download archive.");
return;
}
const opfsRoot = await navigator.storage.getDirectory();
const waczFileHandle = await opfsRoot.getFileHandle(filename, {
create: true,
});
const writable = await waczFileHandle.createWritable();
try {
const reader = response.body!.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
await writable.write(value);
}
} catch (error) {
console.error("Error writing file to OPFS:", error);
alert("Failed to save archive.");
await writable.close();
return;
}
await writable.close();


console.log("WACZ saved to OPFS as:", filename);

// Get a File object from OPFS
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}`);
});
});
}

firstUpdated() {
this.archiveList = document.getElementById(
"archive-list",
) as ArgoArchiveList;

console.log("Archive list:", this.archiveList);
this.registerMessages();
}

Expand Down Expand Up @@ -245,7 +380,10 @@ class ArgoViewer extends LitElement {
this.replayUrl = this.getCollPage() + "#" + params.toString();
}

if (changedProperties.has("pageUrl") || changedProperties.has("failureMsg")) {
if (
changedProperties.has("pageUrl") ||
changedProperties.has("failureMsg")
) {
// @ts-expect-error - TS2339 - Property 'canRecord' does not exist on type 'RecPopup'.
this.canRecord =
// @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'RecPopup'.
Expand Down Expand Up @@ -299,7 +437,9 @@ class ArgoViewer extends LitElement {
render() {
return html`
<md-divider></md-divider>
<div style="padding:1rem; display:flex; align-items:center; justify-content:space-between;">
<div
style="padding:1rem; display:flex; align-items:center; justify-content:space-between;"
>
${
// @ts-expect-error - TS2339 - Property 'recording' does not exist on type 'RecPopup'.
!this.recording
Expand All @@ -316,6 +456,14 @@ class ArgoViewer extends LitElement {
<md-icon slot="icon" style="color:white">public</md-icon>
Resume Archiving
</md-filled-button>

<md-icon-button aria-label="Download" @click=${this.onDownload}>
<md-icon style="color: gray;">download</md-icon>
</md-icon-button>

<md-icon-button aria-label="Share" @click=${this.onShare}>
<md-icon style="color: gray;">share</md-icon>
</md-icon-button>
`
: html`
<md-outlined-button
Expand Down
22 changes: 22 additions & 0 deletions src/types/webtorrent-browser.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
declare module "webtorrent" {
import type { TorrentOptions, Torrent, TorrentFile } from "webtorrent/types";

export default class WebTorrent {
constructor(options?: any);

add(
torrentId: string,
options: TorrentOptions | undefined,
cb: (torrent: Torrent) => void,
): Torrent;

add(torrentId: string, cb: (torrent: Torrent) => void): Torrent;

seed(
input: File | File[] | Blob | Blob[] | string | Buffer,
cb?: (torrent: Torrent) => void,
): Torrent;

destroy(cb?: () => void): void;
}
}
20 changes: 20 additions & 0 deletions src/types/webtorrent-global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// src/types/webtorrent-global.d.ts

interface WebTorrentFile {
name: string;
getBlob(cb: (err: any, blob: Blob) => void): void;
}

interface WebTorrentTorrent {
magnetURI: string;
files: WebTorrentFile[];
}

interface WebTorrentInstance {
add(torrentId: string, callback: (torrent: WebTorrentTorrent) => void): void;
seed(file: File | Blob, callback: (torrent: WebTorrentTorrent) => void): void;
}

interface Window {
WebTorrent: new (...args: any[]) => WebTorrentInstance;
}
16 changes: 16 additions & 0 deletions static/lib/webtorrent.min.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion static/sidepanel.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
}

</style>
<script src="webtorrent.min.js"></script>
<script src="./sidepanel.js"></script>
</head>
<body style="margin: 0; padding: 0; display: flex; flex-direction: column; height: 100vh; overflow: hidden;">
Expand All @@ -82,7 +83,7 @@

<div class="tab-panels" style="flex: 1; overflow-y: auto; position: relative; padding-bottom: 90px;">
<div id="my-archives" class="tab-panel" active>
<argo-archive-list></argo-archive-list>
<argo-archive-list id="archive-list"></argo-archive-list>
</div>
<div id="shared-archives" class="tab-panel">
<!-- future “shared” list… -->
Expand Down
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./src/types"],
"moduleResolution": "node",
"outDir": "./dist/",
"module": "esnext",
"target": "es6",
Expand Down
Loading
Loading