diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index fb011cf..17ac221 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -6,7 +6,12 @@ import { WebContentsView, Menu } from "electron"; import path from "path"; import fs from "fs"; import { Tab, AppState } from "./types"; -import { isValidUrl, sanitizeUrl, getUserAgentForUrl, logSecurityEvent } from "./security"; +import { + isValidUrl, + sanitizeUrl, + getUserAgentForUrl, + logSecurityEvent, +} from "./security"; import { ThemeColorCache } from "./theme-cache"; export class TabManager { @@ -27,6 +32,9 @@ export class TabManager { const webviewPreloadPath = path.join(__dirname, "..", "webview-preload.js"); const hasWebviewPreload = fs.existsSync(webviewPreloadPath); + console.log("[TabManager] Creating tab with preload:", webviewPreloadPath); + console.log("[TabManager] Preload exists:", hasWebviewPreload); + const view = new WebContentsView({ webPreferences: { nodeIntegration: false, @@ -48,8 +56,8 @@ export class TabManager { permission: string, callback: (result: boolean) => void ) => { - if (permission === "media") { - callback(true); // Allow media permissions for DRM + if (permission === "media" || permission === "fullscreen") { + callback(true); // Allow media and fullscreen permissions } else { callback(false); } @@ -88,7 +96,9 @@ export class TabManager { // Hide current active tab and capture its preview if (this.state.activeTabId && this.state.activeTabId !== tabId) { - const currentTab = this.state.tabs.find((t) => t.id === this.state.activeTabId); + const currentTab = this.state.tabs.find( + (t) => t.id === this.state.activeTabId + ); if (currentTab) { // Capture preview before hiding this.captureTabPreview(this.state.activeTabId).catch((err) => { @@ -234,7 +244,10 @@ export class TabManager { /** * Setup WebContentsView event handlers */ - private setupWebContentsViewHandlers(view: WebContentsView, tabId: string): void { + private setupWebContentsViewHandlers( + view: WebContentsView, + tabId: string + ): void { const contents = view.webContents; // Send initial orientation to the new webview when DOM is ready @@ -285,7 +298,10 @@ export class TabManager { url: navigationUrl, }); if (this.state.mainWindow && !this.state.mainWindow.isDestroyed()) { - this.state.mainWindow.webContents.send("navigation-blocked", navigationUrl); + this.state.mainWindow.webContents.send( + "navigation-blocked", + navigationUrl + ); } } else { const userAgent = getUserAgentForUrl(navigationUrl); @@ -310,12 +326,254 @@ export class TabManager { }); this.setupNavigationHandlers(contents, tabId); + this.setupFullscreenHandlers(contents, tabId); + } + + /** + * Setup fullscreen event handlers using Electron's native events (Plan 1.5 - Correct approach) + * Note: We update bounds with gaps and hide status bar in fullscreen mode + */ + private setupFullscreenHandlers( + contents: Electron.WebContents, + tabId: string + ): void { + // Listen for HTML fullscreen API events from Electron + contents.on("enter-html-full-screen", () => { + const tab = this.state.tabs.find((t) => t.id === tabId); + if (!tab) return; + + const timestamp = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${timestamp}] enter-html-full-screen event received` + ); + + // Mark tab as fullscreen (for state tracking) + tab.isFullscreen = true; + + // Update bounds with gaps and hide status bar + if (this.state.mainWindow) { + const windowBounds = this.state.mainWindow.getBounds(); + const topBarHeight = 40; // TOP_BAR_HEIGHT + const deviceFramePadding = 15; // Device frame outer padding + const deviceBorderRadius = 32; // Device frame border radius + + // Calculate safe gap to avoid rounded corners + // Adjust these values to fine-tune fullscreen positioning: + // - Increase to move content away from frame edges + // - Decrease to make content larger (closer to frame edges) + const fullscreenGapVertical = + deviceFramePadding + deviceBorderRadius + 20; // ~67px (Portrait: top/bottom gap) + const fullscreenGapHorizontal = + deviceFramePadding + deviceBorderRadius + 10; // ~57px (Landscape: left/right gap) + + // Determine orientation based on actual window dimensions (not cached state) + const isCurrentlyLandscape = windowBounds.width > windowBounds.height; + + if (isCurrentlyLandscape) { + // Landscape: gap on left and right to avoid rounded corners + // Note: We ignore status bar space in fullscreen mode + const bounds = { + x: fullscreenGapHorizontal - 30, + y: topBarHeight + deviceFramePadding, + width: windowBounds.width - fullscreenGapHorizontal * 2, + height: windowBounds.height - topBarHeight - deviceFramePadding * 2, + }; + tab.view.setBounds(bounds); + } else { + // Portrait: gap on top and bottom to avoid rounded corners + const bounds = { + x: deviceFramePadding, + y: topBarHeight + fullscreenGapVertical - 30, + width: windowBounds.width - deviceFramePadding * 2, + height: + windowBounds.height - + topBarHeight - + fullscreenGapVertical - + fullscreenGapVertical, + }; + tab.view.setBounds(bounds); + } + + // Notify renderer to hide status bar + this.state.mainWindow.webContents.send("fullscreen-mode-changed", true); + + // Force a layout recalculation by resizing the main window + // This ensures WebContentsView properly recalculates its size + const windowBoundsNow = this.state.mainWindow.getBounds(); + this.state.mainWindow.setBounds({ + ...windowBoundsNow, + height: windowBoundsNow.height + 1, + }); + + // Immediately restore to correct size and reapply adjusted bounds + this.state.mainWindow.setBounds(windowBoundsNow); + + // Reapply the adjusted bounds after window resize + if (isCurrentlyLandscape) { + const adjustedBounds = { + x: fullscreenGapHorizontal - 30, + y: topBarHeight + deviceFramePadding, + width: windowBounds.width - fullscreenGapHorizontal * 2, + height: windowBounds.height - topBarHeight - deviceFramePadding * 2, + }; + tab.view.setBounds(adjustedBounds); + } else { + const adjustedBounds = { + x: deviceFramePadding, + y: topBarHeight + fullscreenGapVertical - 30, + width: windowBounds.width - deviceFramePadding * 2, + height: + windowBounds.height - + topBarHeight - + fullscreenGapVertical - + fullscreenGapVertical, + }; + tab.view.setBounds(adjustedBounds); + } + + // Send fullscreen state immediately + if (!tab.view.webContents.isDestroyed()) { + tab.view.webContents.send("set-fullscreen-state", true); + } + } + + }); + + contents.on("leave-html-full-screen", () => { + const tab = this.state.tabs.find((t) => t.id === tabId); + if (!tab) return; + + // Clear fullscreen state + tab.isFullscreen = false; + + // Restore normal bounds + if (this.state.mainWindow) { + this.state.mainWindow.webContents.send( + "fullscreen-mode-changed", + false + ); + + // Restore normal WebContentsView bounds FIRST + const windowBounds = this.state.mainWindow.getBounds(); + const topBarHeight = 40; // TOP_BAR_HEIGHT + const statusBarHeight = 58; + const statusBarWidth = 58; + const frameHalf = 15 / 2; // Device frame padding (half on each side) + + // Determine orientation based on actual window dimensions (not cached state) + const isCurrentlyLandscape = windowBounds.width > windowBounds.height; + + if (isCurrentlyLandscape) { + // Landscape mode: status bar is on the LEFT side + const bounds = { + x: statusBarWidth, + y: Math.round(topBarHeight + frameHalf), + width: Math.round(windowBounds.width - statusBarWidth - frameHalf), + height: Math.round( + windowBounds.height - topBarHeight - frameHalf * 2 + ), + }; + tab.view.setBounds(bounds); + } else { + // Portrait mode: status bar is on the TOP + const bounds = { + x: Math.round(frameHalf), + y: Math.round(topBarHeight + statusBarHeight + frameHalf), + width: Math.round(windowBounds.width - frameHalf * 2), + height: Math.round( + windowBounds.height - + topBarHeight - + statusBarHeight - + frameHalf * 2 + ), + }; + tab.view.setBounds(bounds); + } + + // Force a layout recalculation by resizing the main window + const windowBoundsNow = this.state.mainWindow.getBounds(); + this.state.mainWindow.setBounds({ + ...windowBoundsNow, + height: windowBoundsNow.height + 1, + }); + + // Immediately restore to correct size and reapply adjusted bounds + this.state.mainWindow.setBounds(windowBoundsNow); + + // Reapply the adjusted bounds after window resize + if (isCurrentlyLandscape) { + const adjustedBounds = { + x: statusBarWidth, + y: Math.round(topBarHeight + frameHalf), + width: Math.round(windowBounds.width - statusBarWidth - frameHalf), + height: Math.round( + windowBounds.height - topBarHeight - frameHalf * 2 + ), + }; + tab.view.setBounds(adjustedBounds); + } else { + const adjustedBounds = { + x: Math.round(frameHalf), + y: Math.round(topBarHeight + statusBarHeight + frameHalf), + width: Math.round(windowBounds.width - frameHalf * 2), + height: Math.round( + windowBounds.height - + topBarHeight - + statusBarHeight - + frameHalf * 2 + ), + }; + tab.view.setBounds(adjustedBounds); + } + + // Send fullscreen state immediately + if (!tab.view.webContents.isDestroyed()) { + tab.view.webContents.send("set-fullscreen-state", false); + } + } + + }); + } + + /** + * Exit fullscreen for a specific tab (called by ESC key handler) + */ + exitFullscreen(tabId: string): void { + const tab = this.state.tabs.find((t) => t.id === tabId); + if (!tab || !tab.isFullscreen) return; + + // Execute JavaScript to exit fullscreen in the web page + tab.view.webContents + .executeJavaScript( + ` + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + ` + ) + .catch((err) => { + console.error("[Fullscreen] Failed to exit fullscreen:", err); + }); + + // Notify webview-preload to update state + if (!tab.view.webContents.isDestroyed()) { + tab.view.webContents.send("webview-fullscreen-exited"); + } } /** * Setup navigation event handlers */ - private setupNavigationHandlers(contents: Electron.WebContents, tabId: string): void { + private setupNavigationHandlers( + contents: Electron.WebContents, + tabId: string + ): void { contents.on("did-start-loading", () => { try { const url = contents.getURL(); @@ -380,7 +638,10 @@ export class TabManager { tab.title = contents.getTitle() || url; } - this.state.mainWindow?.webContents.send("webcontents-did-navigate-in-page", url); + this.state.mainWindow?.webContents.send( + "webcontents-did-navigate-in-page", + url + ); if (this.state.activeTabId === tabId && this.state.mainWindow) { this.state.mainWindow.webContents.send("tabs-updated", { @@ -411,7 +672,10 @@ export class TabManager { ); contents.on("render-process-gone", (event: any, details: any) => { - this.state.mainWindow?.webContents.send("webcontents-render-process-gone", details); + this.state.mainWindow?.webContents.send( + "webcontents-render-process-gone", + details + ); }); } } diff --git a/apps/browser/src/main/types.ts b/apps/browser/src/main/types.ts index 42a36a3..b29bd75 100644 --- a/apps/browser/src/main/types.ts +++ b/apps/browser/src/main/types.ts @@ -10,6 +10,8 @@ export interface Tab { title: string; url: string; preview?: string; // Base64 encoded preview image + isFullscreen?: boolean; // Track if this tab is in fullscreen mode + originalBounds?: Electron.Rectangle; // Store original bounds for restoration } export interface AppState { diff --git a/apps/browser/src/main/window-manager.ts b/apps/browser/src/main/window-manager.ts index b3fbaa1..f88be38 100644 --- a/apps/browser/src/main/window-manager.ts +++ b/apps/browser/src/main/window-manager.ts @@ -48,6 +48,44 @@ export class WindowManager { updateWebContentsViewBounds(): void { if (!this.state.webContentsView || !this.state.mainWindow) return; + // Check if active tab is in fullscreen mode (Plan 1.5) + const activeTab = this.state.tabs.find((t) => t.id === this.state.activeTabId); + if (activeTab?.isFullscreen) { + // In fullscreen mode, hide status bar and add gaps to keep within device frame + const windowBounds = this.state.mainWindow.getBounds(); + const topBarHeight = TOP_BAR_HEIGHT; + const deviceFramePadding = FRAME_PADDING / 2; + const fullscreenGapHorizontal = 57; // Match tab-manager + const fullscreenGapVertical = 67; // Match tab-manager + + if (this.state.isLandscape) { + // Landscape: gap on left and right to avoid rounded corners + const bounds = { + x: fullscreenGapHorizontal - 30, + y: topBarHeight + deviceFramePadding, + width: windowBounds.width - fullscreenGapHorizontal * 2, + height: windowBounds.height - topBarHeight - deviceFramePadding * 2, + }; + activeTab.view.setBounds(bounds); + } else { + // Portrait: gap on top and bottom to avoid rounded corners + const bounds = { + x: deviceFramePadding, + y: topBarHeight + fullscreenGapVertical - 30, + width: windowBounds.width - deviceFramePadding * 2, + height: windowBounds.height - topBarHeight - fullscreenGapVertical - fullscreenGapVertical, + }; + activeTab.view.setBounds(bounds); + } + + // Notify renderer to hide status bar in fullscreen mode + this.state.mainWindow.webContents.send("fullscreen-mode-changed", true); + return; + } + + // Not in fullscreen - show status bar + this.state.mainWindow.webContents.send("fullscreen-mode-changed", false); + const bounds = this.state.mainWindow.getBounds(); const dimensions = this.getWindowDimensions(); @@ -147,6 +185,13 @@ export class WindowManager { backgroundColor: "#00000000", roundedCorners: true, resizable: true, + fullscreenable: false, // Prevent window from going fullscreen (Plan 1.5) + }); + + // Prevent window from entering fullscreen when HTML fullscreen is requested + this.state.mainWindow.on("enter-full-screen", () => { + console.log("[Window] Preventing window fullscreen"); + this.state.mainWindow?.setFullScreen(false); }); // Enable swipe navigation gestures on macOS @@ -184,6 +229,7 @@ export class WindowManager { "clipboard-read", "clipboard-write", "media", + "fullscreen", // Allow fullscreen - handled by Electron native events ]; if (allowedPermissions.includes(permission)) { @@ -307,6 +353,18 @@ export class WindowManager { } return; } + + // ESC key to exit fullscreen (Plan 1.5) + if (input.key === "Escape" && !modifierKey && !input.shift && !input.alt) { + if (this.state.activeTabId) { + const activeTab = this.state.tabs.find((t) => t.id === this.state.activeTabId); + if (activeTab?.isFullscreen) { + event.preventDefault(); + this.tabManager.exitFullscreen(this.state.activeTabId); + return; + } + } + } }); } diff --git a/apps/browser/src/preload.ts b/apps/browser/src/preload.ts index 329e625..2aba092 100644 --- a/apps/browser/src/preload.ts +++ b/apps/browser/src/preload.ts @@ -40,6 +40,14 @@ contextBridge.exposeInMainWorld("electronAPI", { return () => ipcRenderer.removeAllListeners("orientation-changed"); }, + // Fullscreen mode listener + onFullscreenModeChanged: (callback: (isFullscreen: boolean) => void) => { + ipcRenderer.on("fullscreen-mode-changed", (_event, isFullscreen) => + callback(isFullscreen) + ); + return () => ipcRenderer.removeAllListeners("fullscreen-mode-changed"); + }, + // Tab management APIs tabs: { getAll: () => ipcRenderer.invoke("tabs-get-all"), diff --git a/apps/browser/src/renderer/app.tsx b/apps/browser/src/renderer/app.tsx index dd50e94..56f6a34 100644 --- a/apps/browser/src/renderer/app.tsx +++ b/apps/browser/src/renderer/app.tsx @@ -17,6 +17,7 @@ function App() { ); const [showTabOverview, setShowTabOverview] = useState(false); const [tabCount, setTabCount] = useState(1); + const [isFullscreen, setIsFullscreen] = useState(false); const webContainerRef = useRef(null); // Initialize and listen for system theme changes @@ -59,6 +60,19 @@ function App() { }; }, []); + // Listen for fullscreen mode changes + useEffect(() => { + const cleanup = window.electronAPI?.onFullscreenModeChanged( + (fullscreen: boolean) => { + setIsFullscreen(fullscreen); + } + ); + + return () => { + if (cleanup) cleanup(); + }; + }, []); + // Track tab count useEffect(() => { // Get initial tab count @@ -73,27 +87,28 @@ function App() { (data: { tabId: string; tabs: any[] }) => { setTabCount(data.tabs.length); - // Set bounds from renderer when tab changes - if (webContainerRef.current) { - const rect = webContainerRef.current.getBoundingClientRect(); - const statusBarHeight = 58; - const statusBarWidth = 58; - - window.electronAPI?.webContents.setBounds({ - x: Math.round( - rect.x + (orientation === "landscape" ? statusBarWidth : 0) - ), - y: Math.round( - rect.y + (orientation === "landscape" ? 0 : statusBarHeight) - ), - width: Math.round( - rect.width - (orientation === "landscape" ? statusBarWidth : 0) - ), - height: Math.round( - rect.height - (orientation === "landscape" ? 0 : statusBarHeight) - ), - }); - } + // Set bounds from renderer when tab changes (skip in fullscreen mode) + // TEMPORARILY DISABLED FOR DEBUGGING + // if (webContainerRef.current && !isFullscreen) { + // const rect = webContainerRef.current.getBoundingClientRect(); + // const statusBarHeight = 58; + // const statusBarWidth = 58; + + // window.electronAPI?.webContents.setBounds({ + // x: Math.round( + // rect.x + (orientation === "landscape" ? statusBarWidth : 0) + // ), + // y: Math.round( + // rect.y + (orientation === "landscape" ? 0 : statusBarHeight) + // ), + // width: Math.round( + // rect.width - (orientation === "landscape" ? statusBarWidth : 0) + // ), + // height: Math.round( + // rect.height - (orientation === "landscape" ? 0 : statusBarHeight) + // ), + // }); + // } } ); @@ -519,6 +534,7 @@ function App() { themeColor={themeColor} textColor={textColor} showTabOverview={showTabOverview} + isFullscreen={isFullscreen} tabOverviewContent={ { window.removeEventListener("resize", updateBounds); }; - }, [webContainerRef, orientation]); + }, [webContainerRef, orientation, isFullscreen]); return (
-
+
{/* Web content area - positioned for WebContentsView */}
- {/* Status bar - React component on top */} - + {/* Status bar - React component on top (hidden in fullscreen) */} + {!isFullscreen && ( + + )} {/* Tab overview overlay - React component */} {showTabOverview && (
diff --git a/apps/browser/src/types/electron-api.d.ts b/apps/browser/src/types/electron-api.d.ts index 6c17fdc..eff96e0 100644 --- a/apps/browser/src/types/electron-api.d.ts +++ b/apps/browser/src/types/electron-api.d.ts @@ -26,7 +26,7 @@ interface Bounds { height: number; } -interface ElectronAPI { +export interface ElectronAPI { platform: NodeJS.Platform; closeWindow: () => void; minimizeWindow: () => void; @@ -40,13 +40,20 @@ interface ElectronAPI { onWebviewReload: (callback: () => void) => () => void; // Theme detection - getSystemTheme: () => Promise<'light' | 'dark'>; - onThemeChanged: (callback: (theme: 'light' | 'dark') => void) => () => void; + getSystemTheme: () => Promise<"light" | "dark">; + onThemeChanged: (callback: (theme: "light" | "dark") => void) => () => void; // Orientation APIs - getOrientation: () => Promise<'portrait' | 'landscape'>; + getOrientation: () => Promise<"portrait" | "landscape">; toggleOrientation: () => Promise; - onOrientationChanged: (callback: (orientation: 'portrait' | 'landscape') => void) => () => void; + onOrientationChanged: ( + callback: (orientation: "portrait" | "landscape") => void + ) => () => void; + + // Fullscreen mode listener + onFullscreenModeChanged: ( + callback: (isFullscreen: boolean) => void + ) => () => void; // Tab management APIs tabs: { @@ -81,7 +88,9 @@ interface ElectronAPI { onDidNavigate: (callback: (url: string) => void) => () => void; onDidNavigateInPage: (callback: (url: string) => void) => () => void; onDomReady: (callback: () => void) => () => void; - onDidFailLoad: (callback: (errorCode: number, errorDescription: string) => void) => () => void; + onDidFailLoad: ( + callback: (errorCode: number, errorDescription: string) => void + ) => () => void; onRenderProcessGone: (callback: (details: any) => void) => () => void; }; } diff --git a/apps/browser/src/webview-preload.ts b/apps/browser/src/webview-preload.ts index afefe75..740ac0d 100644 --- a/apps/browser/src/webview-preload.ts +++ b/apps/browser/src/webview-preload.ts @@ -3,6 +3,173 @@ import { ipcRenderer } from "electron"; +// ============================================================================ +// Fullscreen API Polyfill (Plan 1.5 - Final) +// ============================================================================ +// We need to polyfill the Fullscreen API so web pages think they're in fullscreen +// even though the window doesn't actually go fullscreen + +console.log("[Preload] ✓ Loaded - Fullscreen API polyfill active"); + +// Track fullscreen state +let isFullscreenActive = false; +let fullscreenElement: Element | null = null; + +// Helper function to apply object-fit style to video elements +function applyVideoFitStyle(fitValue: string) { + const videos = document.querySelectorAll('video'); + videos.forEach((video) => { + if (fitValue) { + video.style.objectFit = fitValue; + } else { + video.style.objectFit = ''; + } + }); + + // Also observe for dynamically added videos + if (fitValue === 'contain') { + startVideoObserver(); + } else { + stopVideoObserver(); + } +} + +// MutationObserver to handle dynamically added videos +let videoObserver: MutationObserver | null = null; + +function startVideoObserver() { + if (videoObserver) return; + + videoObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node instanceof HTMLVideoElement) { + node.style.objectFit = 'contain'; + } else if (node instanceof Element) { + const videos = node.querySelectorAll('video'); + videos.forEach((video) => { + (video as HTMLVideoElement).style.objectFit = 'contain'; + }); + } + }); + }); + }); + + videoObserver.observe(document.body, { + childList: true, + subtree: true, + }); +} + +function stopVideoObserver() { + if (videoObserver) { + videoObserver.disconnect(); + videoObserver = null; + } +} + +// Listen for fullscreen state from main process +ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { + const wasFullscreen = isFullscreenActive; + isFullscreenActive = state; + + if (state && !wasFullscreen) { + // Entering fullscreen + fullscreenElement = document.documentElement; // Assume whole document + + // Apply object-fit: contain to all video elements to prevent cropping + applyVideoFitStyle('contain'); + + // Force a resize event to make sure the page knows about the new size + window.dispatchEvent(new Event('resize')); + + const event = new Event("fullscreenchange", { bubbles: true }); + document.dispatchEvent(event); + } else if (!state && wasFullscreen) { + // Exiting fullscreen + fullscreenElement = null; + + // Restore original object-fit style + applyVideoFitStyle(''); + + // Force a resize event + window.dispatchEvent(new Event('resize')); + + const event = new Event("fullscreenchange", { bubbles: true }); + document.dispatchEvent(event); + } +}); + +// Override fullscreenElement getter +Object.defineProperty(Document.prototype, "fullscreenElement", { + get: function(this: Document): Element | null { + return fullscreenElement; + }, + configurable: true, +}); + +// Override webkitFullscreenElement getter +Object.defineProperty(Document.prototype, "webkitFullscreenElement", { + get: function(this: Document): Element | null { + return fullscreenElement; + }, + configurable: true, +}); + +// Store original screen dimensions +const originalScreenWidth = window.screen.width; +const originalScreenHeight = window.screen.height; + +// Override screen.width and screen.height to match window size in fullscreen +Object.defineProperty(window.screen, "width", { + get: function(): number { + // In fullscreen mode, return window size instead of actual screen size + if (isFullscreenActive) { + return window.innerWidth; + } + return originalScreenWidth; + }, + configurable: true, +}); + +Object.defineProperty(window.screen, "height", { + get: function(): number { + // In fullscreen mode, return window size instead of actual screen size + if (isFullscreenActive) { + return window.innerHeight; + } + return originalScreenHeight; + }, + configurable: true, +}); + +// Also override availWidth and availHeight +Object.defineProperty(window.screen, "availWidth", { + get: function(): number { + if (isFullscreenActive) { + return window.innerWidth; + } + return originalScreenWidth; + }, + configurable: true, +}); + +Object.defineProperty(window.screen, "availHeight", { + get: function(): number { + if (isFullscreenActive) { + return window.innerHeight; + } + return originalScreenHeight; + }, + configurable: true, +}); + +console.log("[Preload] ✓ Fullscreen API polyfill installed (with screen size override)"); + +// ============================================================================ +// Theme Color Extraction +// ============================================================================ + // Extract theme color safely when DOM is ready function extractThemeColor(): string | null { try {