import { html, css, wrapCss, IS_APP, apiPrefix } from "replaywebpage";
// replaywebpage imports
import { ReplayWebApp, Embed, Loader } from "replaywebpage";
import { SWManager } from "replaywebpage";
import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.svg";
import fasPlus from "@fortawesome/fontawesome-free/svgs/solid/plus.svg";
import fasUpload from "@fortawesome/fontawesome-free/svgs/solid/upload.svg";
import fasCog from "@fortawesome/fontawesome-free/svgs/solid/cog.svg";
import "./coll";
import "./coll-info";
import "./recordembed";
import "./coll-index";
import { BtrixClient } from "./upload";
import wrRec from "../assets/icons/recLogo.svg";
import awpLogo from "../assets/brand/archivewebpage-icon-color.svg";
import awpBrandLockupColor from "../assets/brand/archivewebpage-lockup-color.svg";
import prettyBytes from "pretty-bytes";
import {
create as createAutoIpfs,
DaemonAPI,
Web3StorageAPI,
// @ts-expect-error - TS7016 - Could not find a declaration file for module 'auto-js-ipfs'. '/Users/emma/Work/Webrecorder/archiveweb.page/node_modules/auto-js-ipfs/index.js' implicitly has an 'any' type.
} from "auto-js-ipfs";
import { getLocalOption, setLocalOption } from "../localstorage";
import { type BtrixOpts } from "../types";
const VERSION = __AWP_VERSION__;
const DEFAULT_GATEWAY_URL = "https://w3s.link/ipfs/";
const DEFAULT_BTRIX_URL = "https://app.browsertrix.com";
//============================================================================
class ArchiveWebApp extends ReplayWebApp {
showCollDrop: boolean;
colls: { id: string; title?: string; loadUrl?: string }[];
autorun: boolean;
settingsError: string;
settingsTab: string;
ipfsOpts: {
daemonUrl: string;
message?: string;
useCustom: boolean;
autoDetect: boolean;
gatewayUrl: string;
};
btrixOpts: BtrixOpts | null;
loadedCollId?: string | null;
showImport?: boolean;
archiveCookies: boolean | null = null;
archiveStorage: boolean | null = null;
archiveFlash: boolean | null = null;
archiveScreenshots: boolean | null = null;
archivePDF: boolean | null = null;
disableMSE: boolean | null = null;
disablePerf: boolean | null = null;
showIpfsShareFailed = false;
showSettings = false;
constructor() {
super();
this.navMenuShown = false;
this.showCollDrop = false;
this.colls = [];
this.autorun = false;
this.settingsError = "";
this.settingsTab = localStorage.getItem("settingsTab") || "prefs";
try {
const res = localStorage.getItem("ipfsOpts");
this.ipfsOpts = JSON.parse(res!);
} catch (e) {
// ignore empty
}
this.ipfsOpts ||= {
daemonUrl: "",
message: "",
useCustom: false,
autoDetect: false,
gatewayUrl: DEFAULT_GATEWAY_URL,
};
try {
const res = localStorage.getItem("btrixOpts");
// @ts-expect-error - TS2339 - Property 'btrixOpts' does not exist on type 'ArchiveWebApp'. | TS2345 - Argument of type 'string | null' is not assignable to parameter of type 'string'.
this.btrixOpts = JSON.parse(res);
this.doBtrixLogin();
} catch (e) {
this.btrixOpts = null;
}
if (window.archivewebpage) {
// @ts-expect-error - TS7006 - Parameter 'progress' implicitly has an 'any' type.
window.archivewebpage.setDownloadCallback((progress) =>
this.onDownloadProgress(progress),
);
}
void this.initOpts();
}
async initOpts() {
this.autorun = (await getLocalOption("autorunBehaviors")) === "1";
const archiveCookies = await getLocalOption("archiveCookies");
// default to true if unset to match existing behavior
if (archiveCookies === null || archiveCookies === undefined) {
await setLocalOption("archiveCookies", "1");
this.archiveCookies = true;
} else {
this.archiveCookies = archiveCookies === "1";
}
this.archiveStorage = (await getLocalOption("archiveStorage")) === "1";
this.archiveFlash = (await getLocalOption("archiveFlash")) === "1";
this.disableMSE = (await getLocalOption("disableMSE")) === "1";
this.disablePerf = (await getLocalOption("disablePerf")) === "1";
const archiveScreenshots = await getLocalOption("archiveScreenshots");
// default to true if unset to enable screenshots!
if (archiveScreenshots === null || archiveScreenshots === undefined) {
await setLocalOption("archiveScreenshots", "1");
this.archiveScreenshots = true;
} else {
this.archiveScreenshots = archiveScreenshots === "1";
}
this.archivePDF = (await getLocalOption("archivePDF")) === "1";
}
async doBtrixLogin() {
try {
// @ts-expect-error - TS2531 - Object is possibly 'null'. | TS2345 - Argument of type 'BtrixOpts | null' is not assignable to parameter of type '{ url: any; username: any; password: any; orgName: any; }'.
this.btrixOpts.client = await BtrixClient.login(this.btrixOpts);
} catch (e) {
this.btrixOpts = null;
}
}
get appName() {
return "ArchiveWeb.page";
}
static get properties() {
return {
...ReplayWebApp.properties,
showStartRecord: { type: Boolean },
showCollDrop: { type: Boolean },
colls: { type: Array },
selCollId: { type: String },
selCollTitle: { type: String },
recordUrl: { type: String },
autorun: { type: Boolean },
showNew: { type: String },
showImport: { type: Boolean },
isImportExisting: { type: Boolean },
loadedCollId: { type: String },
showDownloadProgress: { type: Boolean },
download: { type: Object },
ipfsOpts: { type: Object },
btrixOpts: { type: Object },
uploadCollOpts: { type: Object },
showSettings: { type: Boolean },
settingsTab: { type: String },
settingsError: { type: String },
showIpfsShareFailed: { type: Boolean },
};
}
initRoute() {
const pageParams = new URLSearchParams(window.location.search);
if (pageParams.has("config")) {
super.initRoute();
this.handleMessages();
} else {
this.inited = true;
this.sourceUrl = pageParams.get("source") || "";
}
if (!this.embed) {
this.checkIPFS();
}
}
async checkSW() {
const regs = await navigator.serviceWorker.getRegistrations();
// Remove double SW
for (const reg of regs) {
if (reg.active && reg.active.scriptURL.endsWith("/replay/sw.js")) {
if (await reg.unregister()) {
self.location.reload();
}
}
}
// For App: If no SW, register here
if (IS_APP && !regs.length) {
const qp = new URLSearchParams();
qp.set("injectScripts", "ruffle/ruffle.js");
this.swmanager = new SWManager({
name: this.swName + "?" + qp.toString(),
appName: this.appName,
});
this.swmanager
.register()
.catch(
() =>
(this.swErrorMsg =
this.swmanager?.renderErrorReport(this.mainLogo) || ""),
);
}
}
firstUpdated() {
this.embed = this.pageParams.get("embed") || "";
if (this.embed) {
return super.firstUpdated();
}
this.checkSW();
this.initRoute();
window.addEventListener("popstate", () => {
this.initRoute();
});
}
handleMessages() {
// support upload
window.addEventListener("message", async (event) => {
if (
this.embed &&
this.loadedCollId &&
typeof event.data === "object" &&
event.data.msg_type === "downloadToBlob"
) {
const download = await fetch(
`${apiPrefix}/c/${this.loadedCollId}/dl?format=wacz&pages=all`,
);
const blob = await download.blob();
event.source?.postMessage({
msg_type: "downloadedBlob",
coll: this.loadedCollId,
url: URL.createObjectURL(blob),
});
}
});
}
// @ts-expect-error - TS7006 - Parameter 'event' implicitly has an 'any' type.
onStartLoad(event) {
if (this.embed) {
return;
}
this.showImport = false;
this.sourceUrl = event.detail.sourceUrl;
this.loadInfo = event.detail;
// @ts-expect-error - TS2339 - Property 'isImportExisting' does not exist on type 'ArchiveWebApp'. | TS2339 - Property 'selCollId' does not exist on type 'ArchiveWebApp'.
if (this.isImportExisting && this.selCollId) {
// @ts-expect-error - TS2339 - Property 'loadInfo' does not exist on type 'ArchiveWebApp'. | TS2339 - Property 'selCollId' does not exist on type 'ArchiveWebApp'.
this.loadInfo.importCollId = this.selCollId;
}
}
// @ts-expect-error - TS7006 - Parameter 'event' implicitly has an 'any' type.
onCollLoaded(event) {
if (this.loadInfo?.importCollId) {
if (navigator.serviceWorker.controller) {
const msg = {
msg_type: "reload",
full: true,
name: this.loadInfo.importCollId,
};
navigator.serviceWorker.controller.postMessage(msg);
}
}
if (this.embed) {
this.loadedCollId = event.detail.collInfo?.coll;
}
super.onCollLoaded(event);
if (
!event.detail.alreadyLoaded &&
event.detail.sourceUrl &&
event.detail.sourceUrl !== this.sourceUrl
) {
this.sourceUrl = event.detail.sourceUrl;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getLoadInfo(sourceUrl: string): any {
this.disableCSP();
if (this.loadInfo) {
return this.loadInfo;
}
const customColl = sourceUrl.startsWith("local://")
? sourceUrl.slice("local://".length)
: sourceUrl;
return { customColl };
}
async disableCSP() {
// necessary for chrome 94> up due to new bug introduced
//
// @ts-expect-error - TS2339 - Property 'embed' does not exist on type 'ArchiveWebApp'. | TS2339 - Property 'chrome' does not exist on type 'Window & typeof globalThis'. | TS2339 - Property 'chrome' does not exist on type 'Window & typeof globalThis'.
if (this.embed || !self.chrome?.runtime) {
return;
}
const m = navigator.userAgent.match(/Chrome\/([\d]+)/);
if (!m || Number(m[1]) < 94) {
return;
}
console.log("attempt to disable CSP to ensure replay works");
const tabId = await new Promise((resolve) => {
// @ts-expect-error - TS7006 - Parameter 'msg' implicitly has an 'any' type.
chrome.tabs.getCurrent((msg) => resolve(msg.id));
});
chrome.runtime.sendMessage({
msg: "disableCSP",
tabId,
});
}
static get styles() {
return wrapCss(ArchiveWebApp.appStyles);
}
static get appStyles() {
return wrapCss(css`
:host {
font-size: initial;
overflow: auto;
}
wr-rec-coll {
height: 100%;
width: 100%;
}
.recorder .modal-background {
background-color: rgba(10, 10, 10, 0.5);
}
.recorder .modal-card-head {
background-color: #97a1ff;
}
.extra-padding {
padding: 1em;
}
.less-padding {
padding-top: 1em;
padding-bottom: 1em;
}
div.field.has-addons {
flex: auto;
}
form {
flex-grow: 1;
flex-shrink: 0;
margin: 0px;
}
.dropdown-row {
display: flex;
align-items: center;
margin-bottom: 0.5em;
}
.infomsg {
max-width: 300px;
padding-right: 8px;
}
.rightbar {
margin-left: auto;
display: flex;
}
.dl-progress {
display: flex;
flex-direction: column;
}
@media screen and (max-width: 768px) {
#url {
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
}
.no-pad-mobile {
padding-right: 2px;
}
}
${ReplayWebApp.appStyles}
`);
}
// HACK: returns the logo requested by ReplayWeb.page's nav as nothing now that the new logo includes both graphics and text. Probably best to refactor this behavior.
get mainLogo() {
return "";
}
renderNavEnd() {
return html`
User Guide
{
e.preventDefault();
this.showAbout = true;
}
}"
class="navbar-item is-size-6"
>About
`;
}
renderNavBrand() {
return html` `;
}
renderHomeIndex() {
return html`
Sorry, IPFS sharing / unsharing failed as IPFS could not be reached.
(Check the IPFS settings and try again.)
The download was interrupted
`; case "cancelled": return html`The download was canceled
`; case "completed": return html`Download Completed!
`; } }; return html`ArchiveWeb.page App is a standalone app for Mac, Windows and Linux that allows users to archive webpages as they browse
` : html`ArchiveWeb.page allows users to archive webpages directly in your browser!
`}See the ArchiveWeb.page Guide for more info on how to use this tool.
Full source code is available at: https://github.com/webrecorder/archiveweb.page
ArchiveWeb.page is part of the Webrecorder Project.
ArchiveWeb.page allows users to archive what they browse, storing captured data directly in the browser. Users can downloaded this data as files to their hard drive. Users can also delete any and all archived data at any time. ArchiveWeb.page does not collect any usage or tracking data.
ArchiveWeb.page includes an experimental sharing option for each archive collection. Users can choose to share select archives on a peer-to-peer network (IPFS) via a unique id. Once shared on this network, the data may become accessible to others. All archived items are private and not shared by default, unless explicitly opted-in by the user. (A warning is displayed when sharing via IPFS.)
The application is provided "as is" without any guarantees.
DISCLAIMER OF SOFTWARE WARRANTY. WEBRECORDER SOFTWARE PROVIDES THIS SOFTWARE TO YOU "AS AVAILABLE" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.