diff --git a/src/argo-archive-list.ts b/src/argo-archive-list.ts index 530c7da..9f66c25 100644 --- a/src/argo-archive-list.ts +++ b/src/argo-archive-list.ts @@ -90,9 +90,9 @@ export class ArgoArchiveList extends LitElement { width: 20px !important; height: 20px !important; flex: 0 0 auto; - object-fit: cover; + object-fit: contain; border-radius: 4px; - filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.6)); + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4)); } summary { diff --git a/src/argo-shared-archive-list.ts b/src/argo-shared-archive-list.ts index d205cae..7b6cf84 100644 --- a/src/argo-shared-archive-list.ts +++ b/src/argo-shared-archive-list.ts @@ -93,9 +93,9 @@ export class ArgoSharedArchiveList extends LitElement { width: 20px !important; height: 20px !important; flex: 0 0 auto; - object-fit: cover; + object-fit: contain; border-radius: 4px; - filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.6)); + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4)); } .title-url { diff --git a/src/ext/bg.ts b/src/ext/bg.ts index 0d636b5..8545d5d 100644 --- a/src/ext/bg.ts +++ b/src/ext/bg.ts @@ -9,6 +9,7 @@ import { setLocalOption, getSharedArchives, } from "../localstorage"; +import { isValidUrl } from "../utils"; // =========================================================================== self.recorders = {}; self.newRecId = null; @@ -22,6 +23,7 @@ let newRecCollId = null; let defaultCollId = null; let autorun = false; let isRecordingEnabled = false; +let skipDomains = [] as string[]; const openWinMap = new Map(); @@ -32,6 +34,11 @@ const disabledCSPTabs = new Set(); // @ts-expect-error - TS7034 - Variable 'sidepanelPort' implicitly has type 'any' in some locations where its type cannot be determined. let sidepanelPort = null; +(async function loadSkipDomains() { + // @ts-expect-error + skipDomains = (await getLocalOption("skipDomains")) || []; +})(); + // =========================================================================== function main() { @@ -136,7 +143,7 @@ function sidepanelHandler(port) { //@ts-expect-error tabs has any type async (tabs) => { for (const tab of tabs) { - if (!isValidUrl(tab.url)) continue; + if (!isValidUrl(tab.url, skipDomains)) continue; await startRecorder( tab.id, @@ -217,6 +224,13 @@ chrome.runtime.onMessage.addListener( (message /*sender, sendResponse*/) => { console.log("onMessage", message); switch (message.msg) { + case "optionsChanged": + for (const rec of Object.values(self.recorders)) { + rec.initOpts(); + rec.doUpdateStatus(); + } + break; + case "startNew": (async () => { newRecUrl = message.url; @@ -256,7 +270,7 @@ chrome.tabs.onActivated.addListener(async ({ tabId }) => { chrome.tabs.get(tabId, resolve), ); - if (!isValidUrl(tab.url)) return; + if (!isValidUrl(tab.url, skipDomains)) return; if (!self.recorders[tabId]) { await startRecorder( tabId, @@ -296,7 +310,7 @@ chrome.tabs.onCreated.addListener((tab) => { newRecCollId = null; } else if ( tab.openerTabId && - (!tab.pendingUrl || isValidUrl(tab.pendingUrl)) && + (!tab.pendingUrl || isValidUrl(tab.pendingUrl, skipDomains)) && // @ts-expect-error - TS2339 - Property 'running' does not exist on type 'BrowserRecorder'. self.recorders[tab.openerTabId]?.running ) { @@ -311,7 +325,7 @@ chrome.tabs.onCreated.addListener((tab) => { } if (start) { - if (openUrl && !isValidUrl(openUrl)) { + if (openUrl && !isValidUrl(openUrl, skipDomains)) { return; } startRecorder( @@ -337,9 +351,20 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { openWinMap.delete(changeInfo.url); } + if (changeInfo.url && !isValidUrl(changeInfo.url, skipDomains)) { + stopRecorder(tabId); + delete self.recorders[tabId]; + // let the side-panel know the ’canRecord’/UI state changed + // @ts-expect-error + if (sidepanelPort) { + sidepanelPort.postMessage({ type: "update" }); + } + return; + } + // @ts-expect-error - TS2339 - Property 'waitForTabUpdate' does not exist on type 'BrowserRecorder'. if (recorder.waitForTabUpdate) { - if (isValidUrl(changeInfo.url)) { + if (isValidUrl(changeInfo.url, skipDomains)) { recorder.attach(); } else { // @ts-expect-error - TS2339 - Property 'waitForTabUpdate' does not exist on type 'BrowserRecorder'. @@ -349,9 +374,13 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { } } } else if (changeInfo.url) { + // @ts-expect-error - TS7034 - Variable 'err' implicitly has type 'any' in some locations where its type cannot be determined. + if (sidepanelPort) { + sidepanelPort.postMessage({ type: "update" }); + } if ( isRecordingEnabled && - isValidUrl(changeInfo.url) && + isValidUrl(changeInfo.url, skipDomains) && !self.recorders[tabId] ) { // @ts-expect-error - TS2554 - Expected 2 arguments, but got 3. @@ -361,7 +390,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { if (openWinMap.has(changeInfo.url)) { const collId = openWinMap.get(changeInfo.url); openWinMap.delete(changeInfo.url); - if (!tabId || !isValidUrl(changeInfo.url)) return; + if (!tabId || !isValidUrl(changeInfo.url, skipDomains)) return; // @ts-expect-error - TS2554 - Expected 2 arguments, but got 3. startRecorder(tabId, { collId, autorun }, changeInfo.url); @@ -386,7 +415,7 @@ chrome.contextMenus.onClicked.addListener((info, tab) => { case "toggle-rec": if (!isRecording(tab.id)) { - if (isValidUrl(tab.url)) { + if (isValidUrl(tab.url, skipDomains)) { // @ts-expect-error - TS2554 - Expected 2 arguments, but got 1. startRecorder(tab.id); } @@ -457,17 +486,6 @@ function isRecording(tabId) { return self.recorders[tabId]?.running; } -// =========================================================================== -// @ts-expect-error - TS7006 - Parameter 'url' implicitly has an 'any' type. -function isValidUrl(url) { - return ( - url && - (url === "about:blank" || - url.startsWith("https:") || - url.startsWith("http:")) - ); -} - // =========================================================================== // @ts-expect-error - TS7006 - Parameter 'tabId' implicitly has an 'any' type. async function disableCSPForTab(tabId) { diff --git a/src/ext/browser-recorder.ts b/src/ext/browser-recorder.ts index abaab74..a6ab854 100644 --- a/src/ext/browser-recorder.ts +++ b/src/ext/browser-recorder.ts @@ -2,6 +2,8 @@ import { BEHAVIOR_RUNNING } from "../consts"; import { Recorder } from "../recorder"; +import { isValidUrl } from "../utils"; +import { getLocalOption } from "../localstorage"; // =========================================================================== const DEBUG = false; @@ -333,18 +335,24 @@ class BrowserRecorder extends Recorder { return writtenSize; } - - // @ts-expect-error - TS7006 - Parameter 'pageInfo' implicitly has an 'any' type. - _doAddPage(pageInfo) { + async _doAddPage(pageInfo: { url?: string; [key: string]: any }) { if (!pageInfo.url) { console.warn("Empty Page, Skipping"); return; } + + // @ts-expect-error + const skipDomains: string[] = (await getLocalOption("skipDomains")) || []; + + if (!isValidUrl(pageInfo.url, skipDomains)) { + console.log("Skipping by policy:", pageInfo.url); + return; + } + // @ts-expect-error - TS2339 - Property 'db' does not exist on type 'BrowserRecorder'. if (this.db) { // @ts-expect-error - TS2339 - Property 'db' does not exist on type 'BrowserRecorder'. const result = this.db.addPage(pageInfo); - chrome.runtime.sendMessage({ type: "pageAdded" }); return result; } diff --git a/src/recorder.ts b/src/recorder.ts index f6c0fb0..1955f3d 100644 --- a/src/recorder.ts +++ b/src/recorder.ts @@ -1,5 +1,5 @@ import { RequestResponseInfo } from "./requestresponseinfo"; - +import { isValidUrl, isUrlInSkipList } from "./utils"; import { getCustomRewriter, rewriteDASH, @@ -60,6 +60,7 @@ class Recorder { archiveFlash = false; archiveScreenshots = false; archivePDF = false; + skipDomains: string[] = []; _fetchQueue: FetchEntry[] = []; @@ -163,6 +164,8 @@ class Recorder { this.archiveScreenshots = (await getLocalOption("archiveScreenshots")) === "1"; this.archivePDF = (await getLocalOption("archivePDF")) === "1"; + // @ts-expect-error + this.skipDomains = (await getLocalOption("skipDomains")) || []; } // @ts-expect-error - TS7006 - Parameter 'autorun' implicitly has an 'any' type. @@ -1111,6 +1114,9 @@ class Recorder { // @ts-expect-error - TS7006 - Parameter 'currPage' implicitly has an 'any' type. | TS7006 - Parameter 'domSnapshot' implicitly has an 'any' type. | TS7006 - Parameter 'finished' implicitly has an 'any' type. commitPage(currPage, domSnapshot, finished) { + if (isUrlInSkipList(currPage?.url, this.skipDomains)) { + return; + } if (!currPage?.url || !currPage.ts || currPage.url === "about:blank") { return; } @@ -1135,6 +1141,10 @@ class Recorder { // @ts-expect-error - TS7006 - Parameter 'data' implicitly has an 'any' type. | TS7006 - Parameter 'pageInfo' implicitly has an 'any' type. async commitResource(data, pageInfo) { + if (isUrlInSkipList(data.url, this.skipDomains)) { + return; + } + const payloadSize = data.payload.length; // @ts-expect-error - TS2339 - Property 'pageInfo' does not exist on type 'Recorder'. pageInfo = pageInfo || this.pageInfo; @@ -1552,11 +1562,6 @@ class Recorder { return !status || status === 204 || (status >= 300 && status < 400); } - // @ts-expect-error - TS7006 - Parameter 'url' implicitly has an 'any' type. - isValidUrl(url) { - return url && (url.startsWith("https:") || url.startsWith("http:")); - } - // @ts-expect-error - TS7006 - Parameter 'params' implicitly has an 'any' type. | TS7006 - Parameter 'sessions' implicitly has an 'any' type. async handleLoadingFinished(params, sessions) { const reqresp = this.removeReqResp(params.requestId); @@ -1566,7 +1571,7 @@ class Recorder { return; } - if (!this.isValidUrl(reqresp.url)) { + if (!isValidUrl(reqresp.url, this.skipDomains)) { return; } @@ -1830,7 +1835,7 @@ class Recorder { // @ts-expect-error - TS7006 - Parameter 'request' implicitly has an 'any' type. | TS7006 - Parameter 'sessions' implicitly has an 'any' type. doAsyncFetch(request: FetchEntry, sessions) { - if (!request || !this.isValidUrl(request.url)) { + if (!request || !isValidUrl(request.url, this.skipDomains)) { return; } diff --git a/src/settings-page.ts b/src/settings-page.ts new file mode 100644 index 0000000..c68f311 --- /dev/null +++ b/src/settings-page.ts @@ -0,0 +1,223 @@ +// settings-page.ts +import { LitElement, html, css } from "lit"; +import { customElement } from "lit/decorators.js"; +import "@material/web/textfield/outlined-text-field.js"; +import "@material/web/switch/switch.js"; +import "@material/web/icon/icon.js"; +import "@material/web/iconbutton/icon-button.js"; +import { styles as typescaleStyles } from "@material/web/typography/md-typescale-styles.js"; +import { getLocalOption, setLocalOption } from "./localstorage"; +import { state } from "lit/decorators.js"; + +@customElement("settings-page") +export class SettingsPage extends LitElement { + // @ts-expect-error + static styles: CSSResultGroup = [ + // @ts-expect-error + typescaleStyles as unknown as CSSResultGroup, + css` + :host { + display: flex; + flex-direction: column; + height: 100%; + } + .header { + margin-bottom: 24px; + + & nav { + display: flex; + align-items: center; + margin: 0 16px; + } + } + .content { + box-sizing: border-box; /* include padding in width calculations */ + padding: 16px; /* existing top/bottom padding */ + padding-inline-start: 16px; /* horizontal padding */ + padding-inline-end: 16px; /* horizontal padding */ + overflow-y: auto; + flex: 1; + } + .section { + margin: 24px 16px 0; /* 24px top, 16px left/right, 0 bottom */ + } + .section-label { + display: flex; + justify-content: space-between; + align-items: center; + } + .section-desc { + margin: 4px 0 8px; + color: rgba(0, 0, 0, 0.6); + } + + md-outlined-text-field { + display: block; /* make it a block so margins apply symmetrically */ + box-sizing: border-box; /* include padding/border in its width calculation */ + /* override the inline width:100% you currently have on the element */ + width: auto !important; + /* fill the container minus your 2×16px margins */ + max-width: calc(100% - 32px); + margin: 0 16px; + } + `, + ]; + + @state() + private archiveCookies = false; + @state() + private archiveStorage = false; + @state() + private archiveScreenshots = false; + @state() + private skipDomains = ""; + + connectedCallback() { + super.connectedCallback(); + this.loadSettings(); + } + + private async loadSettings() { + try { + const cookies = await getLocalOption("archiveCookies"); + this.archiveCookies = cookies === "1"; + const storage = await getLocalOption("archiveStorage"); + this.archiveStorage = storage === "1"; + const screenshots = await getLocalOption("archiveScreenshots"); + this.archiveScreenshots = screenshots === "1"; + const domains = await getLocalOption("skipDomains"); + this.skipDomains = Array.isArray(domains) + ? domains.join("\n") + : typeof domains === "string" + ? domains + : ""; + } catch (e) { + console.error("Failed to load settings", e); + } + } + + private async _onSkipDomainsChange(e: Event) { + const textarea = e.currentTarget as HTMLInputElement; + const value = textarea.value; + this.skipDomains = value; // update lit-state so UI stays in sync + + // split into an array, trimming out blank lines + const list = value + .split("\n") + .map((d) => d.trim()) + .filter(Boolean); + + // persist and notify recorder + await setLocalOption("skipDomains", list); + chrome.runtime.sendMessage({ msg: "optionsChanged" }); + } + + private async _onArchiveCookiesChange(e: Event) { + // @ts-expect-error + const checked = (e.currentTarget as HTMLInputElement).selected; + + await setLocalOption("archiveCookies", checked ? "1" : "0"); + chrome.runtime.sendMessage({ msg: "optionsChanged" }); + } + + private async _onArchiveLocalstorageChange(e: Event) { + // @ts-expect-error + const checked = (e.currentTarget as HTMLInputElement).selected; + await setLocalOption("archiveStorage", checked ? "1" : "0"); + chrome.runtime.sendMessage({ msg: "optionsChanged" }); + } + + private async _onArchiveScreenshotsChange(e: Event) { + // @ts-expect-error + const checked = (e.currentTarget as HTMLInputElement).selected; + await setLocalOption("archiveScreenshots", checked ? "1" : "0"); + chrome.runtime.sendMessage({ msg: "optionsChanged" }); + } + + private _onBack() { + this.dispatchEvent( + new CustomEvent("back", { bubbles: true, composed: true }), + ); + } + + render() { + return html` +
+ + +
+ + +
+ +

+ Save a thumbnail screenshot of every page on load. Screenshot will be saved as soon as page is done loading. +

+
+ + +
+ +

+ Archiving cookies may expose private information that is normally + only shared with the site. When enabled, users should exercise + caution about sharing archived pages. +

+
+ +
+ +

+ Archiving local storage will archive information that is generally + always private. +

+ Sharing content created with this setting enabled may compromise your + login credentials. +

+
+ + + + `; + } +} diff --git a/src/sidepanel.ts b/src/sidepanel.ts index 2795982..35d10cb 100644 --- a/src/sidepanel.ts +++ b/src/sidepanel.ts @@ -4,6 +4,7 @@ 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 "./settings-page"; import "@material/web/textfield/outlined-text-field.js"; import "@material/web/icon/icon.js"; import { ArgoArchiveList } from "./argo-archive-list"; @@ -11,6 +12,8 @@ import { Downloader } from "./sw/downloader"; import type { SharedArchive } from "./types"; import { getSharedArchives, setSharedArchives } from "./localstorage"; import { webtorrentClient as client } from "./global-webtorrent"; +import { state } from "lit/decorators.js"; +import { isUrlInSkipList } from "./utils"; import { getLocalOption, @@ -119,6 +122,8 @@ class ArgoViewer extends LitElement { font-size: 14px; font-weight: 500; color: #000; + word-wrap: break-word; + word-break: break-all; } .status-divider { @@ -132,9 +137,9 @@ class ArgoViewer extends LitElement { width: var(--md-icon-size) !important; height: var(--md-icon-size) !important; flex: 0 0 auto; - object-fit: cover; + object-fit: contain; border-radius: 4px; - filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.6)); + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.4)); } /* Fade overlay styles */ @@ -180,6 +185,19 @@ class ArgoViewer extends LitElement { ]; private archiveList!: ArgoArchiveList; + + @state() private showingSettings = false; + @state() private skipDomains: string[] = []; + + private async _toggleSettings() { + this.showingSettings = !this.showingSettings; + if (!this.showingSettings) { + await this.updateComplete; + this.archiveList = this.shadowRoot!.getElementById( + "archive-list", + ) as ArgoArchiveList; + } + } constructor() { super(); // @ts-expect-error - TS2339 - Property 'activeTabIndex' does not exist on type 'ArgoViewer'. @@ -516,9 +534,24 @@ class ArgoViewer extends LitElement { "archive-list", ) as ArgoArchiveList; - console.log("Archive list:", this.archiveList); this.registerMessages(); + // @ts-expect-error + this.skipDomains = await getLocalOption("skipDomains"); + + // @ts-expect-error - TS2339 - Property 'canRecord' does not exist on type 'ArgoViewer'. + this.canRecord = + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + !isUrlInSkipList(this.pageUrl, this.skipDomains) && + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + this.pageUrl && + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + (this.pageUrl === "about:blank" || + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + this.pageUrl.startsWith("http:") || + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + this.pageUrl.startsWith("https:")); + getSharedArchives().then((arr) => { // @ts-expect-error - this.sharedArchives does not exist this.sharedArchives = arr; @@ -528,7 +561,7 @@ class ArgoViewer extends LitElement { await this.reseedAll(); - console.log("Currently seeding (firstUpdated) torrents:", client.torrents); + console.log("Currently seeding torrents:", client.torrents); } updateTabInfo() { @@ -720,6 +753,8 @@ class ArgoViewer extends LitElement { ) { // @ts-expect-error - TS2339 - Property 'canRecord' does not exist on type 'ArgoViewer'. this.canRecord = + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + !isUrlInSkipList(this.pageUrl, this.skipDomains) && // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. this.pageUrl && // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. @@ -863,7 +898,7 @@ class ArgoViewer extends LitElement { style="color: white; border-radius: 9999px; align-self: flex-end;" @click=${this.onShareCurrent} > - share + share Share Current Page ` : "" @@ -1106,6 +1141,11 @@ class ArgoViewer extends LitElement { } render() { + if (this.showingSettings) { + return html``; + } return html` ${this.renderSearch()} ${this.renderTabs()}
@@ -1139,7 +1179,7 @@ class ArgoViewer extends LitElement { ` } - + settings
diff --git a/src/utils.ts b/src/utils.ts index 050b80d..086240c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,36 @@ import { getCollData } from "@webrecorder/wabac"; import { getLocalOption, setLocalOption } from "./localstorage"; +export function isValidUrl(url: string, skipDomains: string[]): Boolean { + if (!isSupportedScheme(url)) { + return false; + } + + if (isUrlInSkipList(url, skipDomains)) { + return false; + } + + return true; +} + +function isSupportedScheme(url: string): boolean { + return ( + url === "about:blank" || url.startsWith("http:") || url.startsWith("https:") + ); +} + +export function isUrlInSkipList(url: string, skipDomains: string[]): boolean { + try { + const host = new URL(url).hostname.toLowerCase(); + return skipDomains.some( + (domain) => host === domain || host.endsWith(`.${domain}`), + ); + } catch (e) { + console.log("utils: Malformed URL in skip check:", e); + return false; + } +} + // =========================================================================== // @ts-expect-error - TS7006 - Parameter 'collLoader' implicitly has an 'any' type. export async function ensureDefaultColl(collLoader) { diff --git a/static/sidepanel.html b/static/sidepanel.html index d704aa8..3ede47b 100644 --- a/static/sidepanel.html +++ b/static/sidepanel.html @@ -24,6 +24,12 @@ --md-checkbox-container-size: 16px; --md-checkbox-icon-size: 16px; + --md-switch-selected-handle-color: var(--md-sys-color-on-primary); + --md-switch-selected-pressed-handle-color: var(--md-sys-color-on-primary); + --md-switch-selected-focus-handle-color: var(--md-sys-color-on-primary); + --md-switch-unselected-pressed-handle-color: var(--md-sys-color-on-primary); + --md-switch-selected-hover-handle-color: var(--md-sys-color-secondary-container); + --md-sys-color-primary: rgb(220, 101, 3); --md-sys-color-surface-tint: rgb(154 70 0); --md-sys-color-on-primary: rgb(255 255 255);