Skip to content

Commit af1ae7c

Browse files
feat: download and share buttons + handling that (#20)
* feat: download and share buttons + handling that * feat: final version * feat: copy magnet link --------- Co-authored-by: nikitalokhmachev-ai <n1kkqt@gmail.com>
1 parent d9e1485 commit af1ae7c

10 files changed

Lines changed: 1316 additions & 33 deletions

package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,38 @@
1111
"author": "Webrecorder Software",
1212
"license": "AGPL-3.0-or-later",
1313
"dependencies": {
14-
"@material/web": "^2.3.0",
1514
"@fortawesome/fontawesome-free": "^5.13.0",
1615
"@ipld/car": "^5.3.2",
1716
"@ipld/unixfs": "^3.0.0",
17+
"@material/web": "^2.3.0",
1818
"@webrecorder/wabac": "^2.22.16",
1919
"auto-js-ipfs": "^2.3.0",
2020
"browsertrix-behaviors": "^0.8.5",
2121
"btoa": "^1.2.1",
22+
"buffer": "^6.0.3",
2223
"bulma": "^0.9.3",
2324
"client-zip": "^2.3.0",
24-
"idb": "^7.1.1",
2525
"hash-wasm": "^4.9.0",
2626
"http-status-codes": "^2.1.4",
27+
"idb": "^7.1.1",
2728
"keyword-mark-element": "^0.1.2",
2829
"node-fetch": "2.6.7",
2930
"p-queue": "^8.0.1",
3031
"pdfjs-dist": "2.2.228",
3132
"pretty-bytes": "^5.6.0",
33+
"process": "^0.11.10",
3234
"replaywebpage": "^2.3.7",
3335
"stream-browserify": "^3.0.0",
3436
"tsconfig-paths-webpack-plugin": "^4.1.0",
3537
"unused-filename": "^4.0.1",
3638
"uuid": "^9.0.0",
37-
"warcio": "^2.4.4"
39+
"warcio": "^2.4.4",
40+
"webtorrent": "^2.6.3"
3841
},
3942
"devDependencies": {
43+
"@types/uuid": "^10.0.0",
4044
"@typescript-eslint/eslint-plugin": "^6.15.0",
4145
"@typescript-eslint/parser": "^6.15.0",
42-
"@types/uuid": "^10.0.0",
4346
"copy-webpack-plugin": "^9.0.1",
4447
"css-loader": "^6.2.0",
4548
"electron": "^32.2.0",

src/argo-archive-list.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,29 @@ export class ArgoArchiveList extends LitElement {
113113
`,
114114
];
115115

116-
@state() private pages: Array<{ ts: string; url: string; title?: string; favIconUrl?: string }> = [];
116+
@state() private pages: Array<{
117+
id: string;
118+
ts: string;
119+
url: string;
120+
title?: string;
121+
favIconUrl?: string;
122+
}> = [];
117123
@state() private collId = "";
124+
@state() private selectedPages = new Set<string>();
125+
126+
private togglePageSelection(ts: string) {
127+
const next = new Set(this.selectedPages);
128+
if (next.has(ts)) {
129+
next.delete(ts);
130+
} else {
131+
next.add(ts);
132+
}
133+
this.selectedPages = next;
134+
}
135+
136+
public getSelectedPages() {
137+
return this.pages.filter((p) => this.selectedPages.has(p.ts));
138+
}
118139

119140
async connectedCallback() {
120141
super.connectedCallback();
@@ -168,12 +189,18 @@ export class ArgoArchiveList extends LitElement {
168189
.map((page) => {
169190
const u = new URL(page.url);
170191
return html`
171-
<md-list-item type="button" @click=${() => this._openPage(page)}>
192+
<md-list-item
193+
type="button"
194+
@click=${() => this._openPage(page)}
195+
>
172196
<div slot="start" class="leading-group">
173197
<md-checkbox
174198
slot="start"
175199
touch-target="wrapper"
176-
@click=${(e: Event) => e.stopPropagation()}
200+
@click=${(e: Event) => {
201+
e.stopPropagation();
202+
this.togglePageSelection(page.ts);
203+
}}
177204
></md-checkbox>
178205
179206
${page.favIconUrl
@@ -215,19 +242,28 @@ export class ArgoArchiveList extends LitElement {
215242
const today = new Date();
216243
const yesterday = new Date(today);
217244
yesterday.setDate(today.getDate() - 1);
218-
const opts: Intl.DateTimeFormatOptions = { weekday: "long", month: "long", day: "numeric", year: "numeric" };
245+
const opts: Intl.DateTimeFormatOptions = {
246+
weekday: "long",
247+
month: "long",
248+
day: "numeric",
249+
year: "numeric",
250+
};
219251
const label = date.toLocaleDateString("en-US", opts);
220252
if (date.toDateString() === today.toDateString()) return `Today — ${label}`;
221-
if (date.toDateString() === yesterday.toDateString()) return `Yesterday — ${label}`;
253+
if (date.toDateString() === yesterday.toDateString())
254+
return `Yesterday — ${label}`;
222255
return label;
223256
}
224257

225258
private _openPage(page: { ts: string; url: string }) {
226-
const tsParam = new Date(Number(page.ts)).toISOString().replace(/[-:TZ.]/g, "");
259+
const tsParam = new Date(Number(page.ts))
260+
.toISOString()
261+
.replace(/[-:TZ.]/g, "");
227262
const urlEnc = encodeURIComponent(page.url);
228263
const fullUrl =
229-
`${chrome.runtime.getURL("index.html")}?source=local://${this.collId}&url=${urlEnc}` +
230-
`#view=pages&url=${urlEnc}&ts=${tsParam}`;
264+
`${chrome.runtime.getURL("index.html")}?source=local://${
265+
this.collId
266+
}&url=${urlEnc}` + `#view=pages&url=${urlEnc}&ts=${tsParam}`;
231267
chrome.tabs.create({ url: fullUrl });
232268
}
233269
}

src/sidepanel.ts

Lines changed: 150 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { unsafeSVG } from "lit/directives/unsafe-svg.js";
55
import "./argo-archive-list";
66
import "@material/web/textfield/outlined-text-field.js";
77
import "@material/web/icon/icon.js";
8+
import { ArgoArchiveList } from "./argo-archive-list";
9+
import { Downloader } from "./sw/downloader";
810

911
import {
1012
getLocalOption,
@@ -22,10 +24,14 @@ import {
2224
import "@material/web/button/filled-button.js";
2325
import "@material/web/button/outlined-button.js";
2426
import "@material/web/divider/divider.js";
27+
import { CollectionLoader } from "@webrecorder/wabac/swlib";
28+
import WebTorrent from "webtorrent";
2529

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

32+
const collLoader = new CollectionLoader();
2833
class ArgoViewer extends LitElement {
34+
private archiveList!: ArgoArchiveList;
2935
constructor() {
3036
super();
3137

@@ -101,7 +107,136 @@ class ArgoViewer extends LitElement {
101107
};
102108
}
103109

110+
private async onDownload() {
111+
const selectedPages = this.archiveList?.getSelectedPages?.() || [];
112+
if (!selectedPages.length) {
113+
alert("Please select some pages to share.");
114+
return;
115+
}
116+
117+
console.log("Selected pages to share:", selectedPages);
118+
119+
const defaultCollId = (await getLocalOption("defaultCollId")) || "";
120+
const coll = await collLoader.loadColl(defaultCollId);
121+
122+
const pageTsList = selectedPages.map((p) => p.id);
123+
const format = "wacz";
124+
const filename = `archive-${Date.now()}.wacz`;
125+
126+
// Webrecorder swlib API format for download:
127+
const downloader = new Downloader({
128+
coll,
129+
format,
130+
filename,
131+
pageList: pageTsList,
132+
});
133+
134+
const response = await downloader.download();
135+
if (!(response instanceof Response)) {
136+
console.error("Download failed:", response);
137+
alert("Failed to download archive.");
138+
return;
139+
}
140+
141+
console.log("Download response:", response);
142+
143+
const blob = await response.blob();
144+
const url = URL.createObjectURL(blob);
145+
146+
// Create temporary <a> to trigger download
147+
const a = document.createElement("a");
148+
a.href = url;
149+
a.download = filename;
150+
document.body.appendChild(a);
151+
a.click();
152+
153+
// Cleanup
154+
URL.revokeObjectURL(url);
155+
document.body.removeChild(a);
156+
157+
console.log("WACZ file downloaded:", filename);
158+
}
159+
160+
private async onShare() {
161+
const selectedPages = this.archiveList?.getSelectedPages?.() || [];
162+
if (!selectedPages.length) {
163+
alert("Please select some pages to share.");
164+
return;
165+
}
166+
167+
console.log("Selected pages to share:", selectedPages);
168+
169+
const defaultCollId = (await getLocalOption("defaultCollId")) || "";
170+
const coll = await collLoader.loadColl(defaultCollId);
171+
172+
const pageTsList = selectedPages.map((p) => p.id);
173+
const format = "wacz";
174+
const filename = `archive-${Date.now()}.wacz`;
175+
176+
// Webrecorder swlib API format for download:
177+
const downloader = new Downloader({
178+
coll,
179+
format,
180+
filename,
181+
pageList: pageTsList,
182+
});
183+
184+
const response = await downloader.download();
185+
if (!(response instanceof Response)) {
186+
console.error("Download failed:", response);
187+
alert("Failed to download archive.");
188+
return;
189+
}
190+
191+
const opfsRoot = await navigator.storage.getDirectory();
192+
const waczFileHandle = await opfsRoot.getFileHandle(filename, {
193+
create: true,
194+
});
195+
const writable = await waczFileHandle.createWritable();
196+
197+
const reader = response.body!.getReader();
198+
while (true) {
199+
const { done, value } = await reader.read();
200+
if (done) break;
201+
await writable.write(value);
202+
}
203+
204+
await writable.close();
205+
206+
console.log("WACZ saved to OPFS as:", filename);
207+
208+
// Get a File object from OPFS
209+
const fileHandle = await opfsRoot.getFileHandle(filename);
210+
const file = await fileHandle.getFile();
211+
212+
// Create a WebTorrent client if not already available
213+
const client = new (window as any).WebTorrent();
214+
215+
// Seed the file
216+
// @ts-expect-error
217+
client.seed(file, (torrent) => {
218+
const magnetURI = torrent.magnetURI;
219+
console.log("Seeding WACZ file via WebTorrent:", magnetURI);
220+
221+
// Copy to clipboard
222+
navigator.clipboard
223+
.writeText(magnetURI)
224+
.then(() => {
225+
alert(`Magnet link copied to clipboard:\n${magnetURI}`);
226+
})
227+
.catch((err) => {
228+
console.error("Failed to copy magnet link:", err);
229+
alert(`Magnet Link Ready:\n${magnetURI}`);
230+
});
231+
});
232+
}
233+
104234
firstUpdated() {
235+
this.archiveList = document.getElementById(
236+
"archive-list",
237+
) as ArgoArchiveList;
238+
239+
console.log("Archive list:", this.archiveList);
105240
this.registerMessages();
106241
}
107242

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

248-
if (changedProperties.has("pageUrl") || changedProperties.has("failureMsg")) {
383+
if (
384+
changedProperties.has("pageUrl") ||
385+
changedProperties.has("failureMsg")
386+
) {
249387
// @ts-expect-error - TS2339 - Property 'canRecord' does not exist on type 'RecPopup'.
250388
this.canRecord =
251389
// @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'RecPopup'.
@@ -299,7 +437,9 @@ class ArgoViewer extends LitElement {
299437
render() {
300438
return html`
301439
<md-divider></md-divider>
302-
<div style="padding:1rem; display:flex; align-items:center; justify-content:space-between;">
440+
<div
441+
style="padding:1rem; display:flex; align-items:center; justify-content:space-between;"
442+
>
303443
${
304444
// @ts-expect-error - TS2339 - Property 'recording' does not exist on type 'RecPopup'.
305445
!this.recording
@@ -316,6 +456,14 @@ class ArgoViewer extends LitElement {
316456
<md-icon slot="icon" style="color:white">public</md-icon>
317457
Resume Archiving
318458
</md-filled-button>
459+
460+
<md-icon-button aria-label="Download" @click=${this.onDownload}>
461+
<md-icon style="color: gray;">download</md-icon>
462+
</md-icon-button>
463+
464+
<md-icon-button aria-label="Share" @click=${this.onShare}>
465+
<md-icon style="color: gray;">share</md-icon>
466+
</md-icon-button>
319467
`
320468
: html`
321469
<md-outlined-button

src/types/webtorrent-browser.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
declare module "webtorrent" {
2+
import type { TorrentOptions, Torrent, TorrentFile } from "webtorrent/types";
3+
4+
export default class WebTorrent {
5+
constructor(options?: any);
6+
7+
add(
8+
torrentId: string,
9+
options: TorrentOptions | undefined,
10+
cb: (torrent: Torrent) => void,
11+
): Torrent;
12+
13+
add(torrentId: string, cb: (torrent: Torrent) => void): Torrent;
14+
15+
seed(
16+
input: File | File[] | Blob | Blob[] | string | Buffer,
17+
cb?: (torrent: Torrent) => void,
18+
): Torrent;
19+
20+
destroy(cb?: () => void): void;
21+
}
22+
}

src/types/webtorrent-global.d.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// src/types/webtorrent-global.d.ts
2+
3+
interface WebTorrentFile {
4+
name: string;
5+
getBlob(cb: (err: any, blob: Blob) => void): void;
6+
}
7+
8+
interface WebTorrentTorrent {
9+
magnetURI: string;
10+
files: WebTorrentFile[];
11+
}
12+
13+
interface WebTorrentInstance {
14+
add(torrentId: string, callback: (torrent: WebTorrentTorrent) => void): void;
15+
seed(file: File | Blob, callback: (torrent: WebTorrentTorrent) => void): void;
16+
}
17+
18+
interface Window {
19+
WebTorrent: new (...args: any[]) => WebTorrentInstance;
20+
}

static/lib/webtorrent.min.js

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

static/sidepanel.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
}
6060

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

8384
<div class="tab-panels" style="flex: 1; overflow-y: auto; position: relative; padding-bottom: 90px;">
8485
<div id="my-archives" class="tab-panel" active>
85-
<argo-archive-list></argo-archive-list>
86+
<argo-archive-list id="archive-list"></argo-archive-list>
8687
</div>
8788
<div id="shared-archives" class="tab-panel">
8889
<!-- future “shared” list… -->

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
22
"compilerOptions": {
3+
"typeRoots": ["./node_modules/@types", "./src/types"],
4+
"moduleResolution": "node",
35
"outDir": "./dist/",
46
"module": "esnext",
57
"target": "es6",

0 commit comments

Comments
 (0)