| 1 | import type { Rectangle, MouseInputEvent } from "electron"; |
| 2 | import type { GuestPage, GuestView, GuestViewEvents, GuestViewFactory } from "./guestView.js"; |
| 3 | import type { BrowserViewport } from "./viewport.js"; |
| 4 | import type { RecoveredTab } from "./recoveryStore.js"; |
| 5 | import { browserFailure } from "./errors.js"; |
| 6 | |
| 7 | // A restored logical tab has no WebContents until explicit UI or granted access. |
| 8 | export class LazyGuestView implements GuestView { |
| 9 | private live: GuestView | null = null; |
| 10 | private events?: GuestViewEvents; |
| 11 | private bounds?: Rectangle; |
| 12 | private visible = false; |
| 13 | private destroyed = false; |
| 14 | private loading: Promise<void> | null = null; |
| 15 | private viewport: BrowserViewport | null; |
| 16 | readonly page: GuestPage; |
| 17 | constructor(private readonly factory: GuestViewFactory, private readonly partition: string, private readonly restored: RecoveredTab) { |
| 18 | this.viewport = restored.viewport; |
| 19 | this.page = new Proxy({} as GuestPage, { get: (_target, key) => { |
| 20 | if (key === "loadURL") return (url: string) => { this.loading = this.materialize().page.loadURL(url); return this.loading; }; |
| 21 | if (this.live) { |
| 22 | const value = Reflect.get(this.live.page, key); |
| 23 | return typeof value === "function" ? value.bind(this.live.page) : value; |
| 24 | } |
| 25 | if (key === "id") return -1; |
| 26 | if (key === "getURL") return () => restored.url; |
| 27 | if (key === "getTitle") return () => restored.title; |
| 28 | if (key === "isDestroyed") return () => this.destroyed; |
| 29 | if (key === "isLoading") return () => false; |
| 30 | if (key === "getZoomFactor") return () => 1; |
| 31 | if (key === "navigationHistory") return { canGoBack: () => false, canGoForward: () => false, goBack() {}, goForward() {} }; |
| 32 | throw browserFailure("page_not_ready", "restored page has not been loaded"); |
| 33 | } }); |
| 34 | } |
| 35 | isPlaceholder(): boolean { return this.live === null; } |
| 36 | private materialize(): GuestView { |
| 37 | if (this.destroyed) throw browserFailure("stale_document", "restored tab was closed"); |
| 38 | if (!this.live) { |
| 39 | this.live = this.factory.create(this.partition); |
| 40 | if (this.events) this.live.bind(this.events); |
| 41 | if (this.bounds) this.live.setBounds(this.bounds); |
| 42 | this.live.setViewport?.(this.viewport); |
| 43 | this.live.setVisible(this.visible); |
| 44 | } |
| 45 | return this.live; |
| 46 | } |
| 47 | ensureLoaded(): Promise<void> { |
| 48 | if (this.restored.fileReference && !this.live) return Promise.reject(browserFailure("page_not_ready", "local preview needs fresh file authorization; use Reopen preview")); |
| 49 | if (this.live) return this.loading ?? Promise.resolve(); |
| 50 | if (!this.loading) this.loading = this.materialize().page.loadURL(this.restored.url); |
| 51 | return this.loading; |
| 52 | } |
| 53 | bind(events: GuestViewEvents): void { this.events = events; this.live?.bind(events); } |
| 54 | setBounds(bounds: Rectangle): void { this.bounds = bounds; this.live?.setBounds(bounds); } |
| 55 | setVisible(visible: boolean): void { this.visible = visible; this.live?.setVisible(visible); } |
| 56 | setViewport(viewport: BrowserViewport | null): void { this.viewport = viewport; this.live?.setViewport?.(viewport); } |
| 57 | inputScale(): number { return this.live?.inputScale?.() ?? 1; } |
| 58 | capturePixelRatio(): number { return this.live?.capturePixelRatio?.() ?? 1; } |
| 59 | presentForUser(): void { this.live?.presentForUser?.(); } |
| 60 | prepareObservation(): () => void { return this.materialize().prepareObservation?.() ?? (() => {}); } |
| 61 | async sendMouseInput(event: MouseInputEvent, verify?: () => void): Promise<void> { const view = this.materialize(); if (view.sendMouseInput) await view.sendMouseInput(event, verify); else { verify?.(); view.page.sendInputEvent(event); } } |
| 62 | captureSurfaceSize(): { width: number; height: number } { return this.live?.captureSurfaceSize?.() ?? this.bounds ?? { width: 1280, height: 720 }; } |
| 63 | get diagnostics() { return this.live?.diagnostics; } |
| 64 | async prepareCapture(signal: AbortSignal, recording = false): Promise<() => void> { return await this.materialize().prepareCapture?.(signal, recording) ?? (() => {}); } |
| 65 | destroy(): void { this.destroyed = true; this.live?.destroy(); } |
| 66 | } |
| 67 |