diff --git a/src/argo-archive-list.ts b/src/argo-archive-list.ts index 9f66c25..6fa56cf 100644 --- a/src/argo-archive-list.ts +++ b/src/argo-archive-list.ts @@ -7,7 +7,6 @@ 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"; diff --git a/src/argo-shared-archive-list.ts b/src/argo-shared-archive-list.ts index 7b6cf84..87e9baa 100644 --- a/src/argo-shared-archive-list.ts +++ b/src/argo-shared-archive-list.ts @@ -8,7 +8,6 @@ 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"; diff --git a/src/assets/brand/packrat-lockup-white.svg b/src/assets/brand/packrat-lockup-white.svg new file mode 100644 index 0000000..304225a --- /dev/null +++ b/src/assets/brand/packrat-lockup-white.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/downloading.avif b/src/assets/images/downloading.avif new file mode 100644 index 0000000..096aab3 Binary files /dev/null and b/src/assets/images/downloading.avif differ diff --git a/src/assets/images/forest.avif b/src/assets/images/forest.avif new file mode 100644 index 0000000..90aa901 Binary files /dev/null and b/src/assets/images/forest.avif differ diff --git a/src/assets/images/sharing-warning.avif b/src/assets/images/sharing-warning.avif new file mode 100644 index 0000000..e7ed45b Binary files /dev/null and b/src/assets/images/sharing-warning.avif differ diff --git a/src/assets/images/sharing.avif b/src/assets/images/sharing.avif new file mode 100644 index 0000000..a7fb48e Binary files /dev/null and b/src/assets/images/sharing.avif differ diff --git a/src/ext/bg.ts b/src/ext/bg.ts index 8545d5d..f252878 100644 --- a/src/ext/bg.ts +++ b/src/ext/bg.ts @@ -95,6 +95,15 @@ function sidepanelHandler(port) { // @ts-expect-error - TS2339 - Property 'port' does not exist on type 'BrowserRecorder'. self.recorders[tabId].port = port; self.recorders[tabId].doUpdateStatus(); + } else if (isRecordingEnabled) { + // Send the current recording state even if no recorder exists for this tab + port.postMessage({ + type: "status", + recording: false, // No recorder for this tab + autorun, + // @ts-expect-error + collId: defaultCollId, + }); } port.postMessage(await listAllMsg(collLoader)); break; @@ -353,8 +362,18 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { if (changeInfo.url && !isValidUrl(changeInfo.url, skipDomains)) { stopRecorder(tabId); + + // Ensure debugger is detached when navigating to a skipped domain + try { + chrome.debugger.detach({ tabId }, () => { + // Debugger detached, ignore any errors as it might already be detached + }); + } catch (e) { + // Ignore errors - debugger might already be detached + } + delete self.recorders[tabId]; - // let the side-panel know the ’canRecord’/UI state changed + // let the side-panel know the 'canRecord'/UI state changed // @ts-expect-error if (sidepanelPort) { sidepanelPort.postMessage({ type: "update" }); @@ -440,6 +459,9 @@ async function startRecorder(tabId, opts) { let err = null; // @ts-expect-error - TS7034 - Variable 'sidepanelPort' implicitly has type 'any' in some locations where its type cannot be determined. if (sidepanelPort) { + // Set the port on the recorder so it can send status updates + // @ts-expect-error + self.recorders[tabId].port = sidepanelPort; sidepanelPort.postMessage({ type: "update" }); } const { waitForTabUpdate } = opts; @@ -449,9 +471,28 @@ async function startRecorder(tabId, opts) { try { self.recorders[tabId].setCollId(opts.collId); await self.recorders[tabId].attach(); + + // Send status update after successful attach + // @ts-expect-error + if (sidepanelPort && self.recorders[tabId]) { + self.recorders[tabId].doUpdateStatus(); + } } catch (e) { console.warn(e); err = e; + + // Clean up on error + // @ts-expect-error + if (err?.message?.includes("already attached")) { + // Try to detach and delete the recorder + try { + chrome.debugger.detach({ tabId }, () => { + delete self.recorders[tabId]; + }); + } catch (detachErr) { + console.warn("Failed to detach debugger:", detachErr); + } + } } return err; } @@ -462,6 +503,19 @@ async function startRecorder(tabId, opts) { function stopRecorder(tabId) { if (self.recorders[tabId]) { self.recorders[tabId].detach(); + + // Ensure the sidepanel is notified about the stop + // @ts-expect-error - TS7034 - Variable 'sidepanelPort' implicitly has type 'any' in some locations where its type cannot be determined. + if (sidepanelPort) { + sidepanelPort.postMessage({ + type: "status", + recording: false, + autorun, + // @ts-expect-error - defaultCollId implicitly has an 'any' type. + collId: defaultCollId, + }); + } + return true; } diff --git a/src/globals.d.ts b/src/globals.d.ts index 09d3061..c00e7b5 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -1,4 +1,7 @@ declare module "*.svg"; +declare module "*.png"; +declare module "*.avif" +declare module "*.jpg"; declare module "*.html"; declare module "*.scss"; declare module "*.sass"; diff --git a/src/onboarding.ts b/src/onboarding.ts new file mode 100644 index 0000000..c00f118 --- /dev/null +++ b/src/onboarding.ts @@ -0,0 +1,327 @@ +import { LitElement, html, css, unsafeCSS, CSSResultGroup } from "lit"; +import { unsafeSVG } from "lit/directives/unsafe-svg.js"; + +import { customElement, property, state } from "lit/decorators.js"; +import { styles as typescaleStyles } from "@material/web/typography/md-typescale-styles.js"; + +// Import Material Design components +import "@material/web/button/filled-button.js"; +import "@material/web/button/outlined-button.js"; +import "@material/web/divider/divider.js"; +import "@material/web/icon/icon.js"; + +// Import assets +import forestImg from "./assets/images/forest.avif"; +import packratLogo from "./assets/brand/packrat-lockup-white.svg"; +import downloadingImg from "./assets/images/downloading.avif"; +import sharingImg from "./assets/images/sharing.avif"; +import warningImg from "./assets/images/sharing-warning.avif"; + +@customElement("wr-onboarding") +export class OnboardingView extends LitElement { + static styles: CSSResultGroup = [ + typescaleStyles as unknown as CSSResultGroup, + css` + :host { + position: relative; + display: flex; + flex-direction: column; + overflow: hidden; /* Changed from scroll to hidden */ + width: 100vw; + height: 100vh; + background: url(${unsafeCSS(forestImg)}) center/cover no-repeat; + } + + .slides-container { + flex: 1; + overflow: hidden; + position: relative; + } + + .slides { + display: flex; + align-items: center; + justify-content: flex-start; /* Changed from center */ + height: 100%; + transition: transform 500ms ease-in-out; + padding: 2rem; + gap: 2rem; + box-sizing: border-box; + } + + /* Transform classes for the slides container */ + .slides.step-0 { + transform: translateX(0); + } + + .slides.step-1 { + transform: translateX(calc(-100vw + 2rem)); + } + + .slides.step-2 { + transform: translateX(calc(-200vw + 4rem)); + } + + .slides.step-3 { + transform: translateX(calc(-300vw + 6rem)); + } + + .slide { + width: calc(100vw - 4rem); + height: 100%; + box-sizing: border-box; + padding: 2rem; + background: var(--md-sys-color-surface); + border-radius: 0.5rem; + box-shadow: var(--md-sys-elevation-level2); + display: flex; + flex-direction: column; + flex-shrink: 0; /* Prevent slides from shrinking */ + opacity: 1; + } + + .slide.hidden { + opacity: 0; + transition: opacity 2s ease-out; + } + + /* First slide - full screen, no card styling */ + .slide.first { + background: transparent; + box-shadow: none; + border-radius: 0; + padding: 0; + height: 100%; + } + + /* First slide content */ + .first-content { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3rem; + } + + .first-content .logo { + width: 100%; + max-width: 256px; + height: auto; + } + + /* Card content */ + .card-content { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 1rem; + max-height: 100%; + } + + .card-content-imgcontainer { + width: 100%; + flex-shrink: 1; + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.12)) + drop-shadow(0 1px 2px rgba(0, 0, 0, 0.24)); + } + + .card-content img { + width: 100%; + height: 100%; + object-fit: contain; /* or 'cover', depending on your goal */ + display: block; + } + + .card-content md-divider { + width: 100%; + margin: 0.5rem 0; + } + + /* Dots indicator */ + .dots { + display: flex; + justify-content: center; + gap: 0.5rem; + } + + .dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--md-sys-color-outline-variant); + transition: background 200ms; + } + + .dot[active] { + background: var(--md-sys-color-primary); + } + + /* Bottom navigation panel */ + .bottom-panel { + background: var(--md-sys-color-surface); + box-shadow: var(--md-sys-elevation-level2); + } + + .bottom-panel-content { + padding: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + } + + .bottom-panel md-outlined-button { + --md-outlined-button-container-shape: 999px; + } + + .bottom-panel md-filled-button { + --md-filled-button-container-shape: 999px; + color: white; + } + + .bottom-panel md-outlined-button[disabled] { + opacity: 0.38; + } + + /* Hide bottom panel on first slide */ + .bottom-panel[hidden] { + display: none; + } + `, + ]; + + @state() private step = 0; + + private content = [ + { + first: true, + }, + { + title: "Packrat downloads websites as you browse", + img: downloadingImg, + body: "All the pages you view with the extension enabled will be saved locally to your computer.", + alt: "Digital collage of content coalessing into a vintage computer monitor", + }, + { + title: "Share your archives with others with a link", + img: sharingImg, + body: "All data is transferred directly from your computer to their browser. Nobody else gets access to your archives.", + alt: "Digital collage of two computers with images being transferred between them in a whirlwind of scribbles", + }, + { + title: "Web archives can contain private data!", + img: warningImg, + body: "Web archives of logged-in sites can contain private messages, user data, and account credentials. Only share archives of logged-in sites with people you trust.", + alt: "Digital collage of a top secret classified document cover sheet atop maps and an envelope", + }, + ]; + + private _prev() { + if (this.step > 1) { + this.step--; + } + } + + private _next() { + if (this.step < this.content.length - 1) { + this.step++; + } else { + this.dispatchEvent(new CustomEvent("completed", { bubbles: true })); + } + } + + render() { + return html` +
+
+ ${this.content.map( + (slide, i) => html` +
+ ${i === 0 + ? html` +
+ + + Get Started + +
+ ` + : html` +
+
+ ${slide.alt} +
+
+

+ ${slide.title} +

+ +

+ ${slide.body} +

+
+
+ ${this.content + .slice(1) + .map( + (_, j) => html` +
+ `, + )} +
+
+ `} +
+ `, + )} +
+
+ +
+ +
+ + arrow_back + Previous + + + + ${this.step === this.content.length - 1 + ? "Get Started!" + : html` + Next + arrow_forward + `} + +
+
+ `; + } +} diff --git a/src/settings-page.ts b/src/settings-page.ts index c68f311..bace4a8 100644 --- a/src/settings-page.ts +++ b/src/settings-page.ts @@ -71,6 +71,8 @@ export class SettingsPage extends LitElement { private archiveScreenshots = false; @state() private skipDomains = ""; + @state() + private showOnboarding = false; connectedCallback() { super.connectedCallback(); @@ -91,6 +93,8 @@ export class SettingsPage extends LitElement { : typeof domains === "string" ? domains : ""; + const onb = await getLocalOption("showOnboarding"); + this.showOnboarding = onb !== "0"; } catch (e) { console.error("Failed to load settings", e); } @@ -134,6 +138,14 @@ export class SettingsPage extends LitElement { chrome.runtime.sendMessage({ msg: "optionsChanged" }); } + private async _onShowOnboardingChange(e: Event) { + // @ts-expect-error md-switch uses `selected` for its checked state + const checked = (e.currentTarget as HTMLInputElement).selected; + this.showOnboarding = checked; + await setLocalOption("showOnboarding", checked ? "1" : "0"); + chrome.runtime.sendMessage({ msg: "optionsChanged" }); + } + private _onBack() { this.dispatchEvent( new CustomEvent("back", { bubbles: true, composed: true }), @@ -216,6 +228,21 @@ export class SettingsPage extends LitElement {

+
+ +

+ When enabled, the onboarding carousel will run the next time you open the side panel. +

+
+ `; diff --git a/src/sidepanel.ts b/src/sidepanel.ts index 35d10cb..826342e 100644 --- a/src/sidepanel.ts +++ b/src/sidepanel.ts @@ -5,6 +5,7 @@ import { unsafeSVG } from "lit/directives/unsafe-svg.js"; import "./argo-archive-list"; import "./argo-shared-archive-list"; import "./settings-page"; +import "./onboarding"; import "@material/web/textfield/outlined-text-field.js"; import "@material/web/icon/icon.js"; import { ArgoArchiveList } from "./argo-archive-list"; @@ -186,18 +187,36 @@ class ArgoViewer extends LitElement { private archiveList!: ArgoArchiveList; + @state() private showOnboarding = false; @state() private showingSettings = false; + @state() private isBlocked = false; @state() private skipDomains: string[] = []; private async _toggleSettings() { this.showingSettings = !this.showingSettings; + + // when toggling *off* settings, reload skip-list and re-query if (!this.showingSettings) { + // re-load the list from storage + // @ts-expect-error + this.skipDomains = await getLocalOption("skipDomains"); + + // re-run your normal "update everything" flow + this.updateTabInfo(); + + // wait for the archive list element to re-render await this.updateComplete; this.archiveList = this.shadowRoot!.getElementById( "archive-list", ) as ArgoArchiveList; } } + + private async _onboardingDone() { + // turn the flag off so onboarding won’t show again + await setLocalOption("showOnboarding", "0"); + this.showOnboarding = false; + } constructor() { super(); // @ts-expect-error - TS2339 - Property 'activeTabIndex' does not exist on type 'ArgoViewer'. @@ -539,6 +558,9 @@ class ArgoViewer extends LitElement { // @ts-expect-error this.skipDomains = await getLocalOption("skipDomains"); + const onb = await getLocalOption("showOnboarding"); + this.showOnboarding = onb !== "0"; + // @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'. @@ -749,8 +771,15 @@ class ArgoViewer extends LitElement { if ( changedProperties.has("pageUrl") || - changedProperties.has("failureMsg") + changedProperties.has("failureMsg") || + changedProperties.has("skipDomains") ) { + this.isBlocked = + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + !!this.pageUrl && + // exactly the same check you use for skipDomains + // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. + isUrlInSkipList(this.pageUrl, this.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'. @@ -804,10 +833,6 @@ class ArgoViewer extends LitElement { this.waitingForStop = true; } - get notRecordingMessage() { - return "Archiving Disabled"; - } - renderStatusCard() { return html`
@@ -937,6 +962,20 @@ class ArgoViewer extends LitElement { `; } + if (this.isBlocked) { + return html` + Status +
+ block + + Archiving blocked by your block-list. + +
+ `; + } + // @ts-expect-error - TS2339 - Property 'canRecord' does not exist on type 'ArgoViewer'. if (!this.canRecord) { // @ts-expect-error - TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. | TS2339 - Property 'pageUrl' does not exist on type 'ArgoViewer'. @@ -977,7 +1016,7 @@ class ArgoViewer extends LitElement { folder_off - ${this.notRecordingMessage} + Archiving Disabled
`; } @@ -1141,6 +1180,12 @@ class ArgoViewer extends LitElement { } render() { + if (this.showOnboarding) { + return html``; + } + if (this.showingSettings) { return html`