From 5bf22e0db7706f9c9508440ac6589f34c60abd7a Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 15:53:29 +0900 Subject: [PATCH 01/10] feat: implement HTML5 fullscreen support with status bar hiding and gap adjustments #70 --- apps/browser/src/main/tab-manager.ts | 133 +++++++++++++++++- apps/browser/src/main/types.ts | 2 + apps/browser/src/main/window-manager.ts | 54 +++++++ apps/browser/src/preload.ts | 8 ++ apps/browser/src/renderer/app.tsx | 15 ++ .../src/renderer/components/phone-frame.tsx | 16 ++- apps/browser/src/types/electron-api.d.ts | 3 + apps/browser/src/webview-preload.ts | 52 +++++++ 8 files changed, 275 insertions(+), 8 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index fb011cf..7513436 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -27,6 +27,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 +51,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); } @@ -310,6 +313,132 @@ 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; + + console.log("[Fullscreen] 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 fullscreenGap = 50; // 50px gap + + if (this.state.isLandscape) { + // Landscape: 50px gap on left and right + tab.view.setBounds({ + x: fullscreenGap, + y: topBarHeight, + width: windowBounds.width - (fullscreenGap * 2), + height: windowBounds.height - topBarHeight, + }); + } else { + // Portrait: 50px gap on top and bottom + tab.view.setBounds({ + x: 0, + y: topBarHeight + fullscreenGap, + width: windowBounds.width, + height: windowBounds.height - topBarHeight - (fullscreenGap * 2), + }); + } + + // Notify renderer to hide status bar + this.state.mainWindow.webContents.send("fullscreen-mode-changed", true); + + // Notify webview that it's in fullscreen state (for CSS/JS) + tab.view.webContents.send("set-fullscreen-state", true); + } + + console.log("[Fullscreen] ✅ Fullscreen mode enabled (with gaps, status bar hidden)"); + }); + + contents.on("leave-html-full-screen", () => { + const tab = this.state.tabs.find((t) => t.id === tabId); + if (!tab) return; + + console.log("[Fullscreen] leave-html-full-screen event received"); + + // Clear fullscreen state + tab.isFullscreen = false; + + // Restore normal bounds + if (this.state.mainWindow) { + this.state.mainWindow.webContents.send("fullscreen-mode-changed", false); + + // Notify webview that it's no longer in fullscreen state + tab.view.webContents.send("set-fullscreen-state", false); + + // Restore normal WebContentsView bounds + const windowBounds = this.state.mainWindow.getBounds(); + const topBarHeight = 40; // TOP_BAR_HEIGHT + const statusBarHeight = 58; + const statusBarWidth = 58; + const framePadding = 15; // Device frame padding + + if (this.state.isLandscape) { + // Landscape mode + tab.view.setBounds({ + x: statusBarWidth + framePadding, + y: topBarHeight + framePadding, + width: windowBounds.width - statusBarWidth - framePadding * 2, + height: windowBounds.height - topBarHeight - framePadding * 2, + }); + } else { + // Portrait mode + tab.view.setBounds({ + x: framePadding, + y: topBarHeight + statusBarHeight + framePadding, + width: windowBounds.width - framePadding * 2, + height: windowBounds.height - topBarHeight - statusBarHeight - framePadding * 2, + }); + } + } + + console.log("[Fullscreen] ✅ Fullscreen state cleared (status bar shown, bounds restored)"); + }); + } + + /** + * 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; + + console.log("[Fullscreen] Exiting fullscreen via ESC key"); + + // 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"); + } } /** 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..58acb0b 100644 --- a/apps/browser/src/main/window-manager.ts +++ b/apps/browser/src/main/window-manager.ts @@ -48,6 +48,40 @@ 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 fullscreenGap = 50; // 50px gap for fullscreen mode + + if (this.state.isLandscape) { + // Landscape: 50px gap on left and right + activeTab.view.setBounds({ + x: fullscreenGap, + y: topBarHeight, + width: windowBounds.width - (fullscreenGap * 2), + height: windowBounds.height - topBarHeight, + }); + } else { + // Portrait: 50px gap on top and bottom + activeTab.view.setBounds({ + x: 0, + y: topBarHeight + fullscreenGap, + width: windowBounds.width, + height: windowBounds.height - topBarHeight - (fullscreenGap * 2), + }); + } + + // 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 +181,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 +225,7 @@ export class WindowManager { "clipboard-read", "clipboard-write", "media", + "fullscreen", // Allow fullscreen - handled by Electron native events ]; if (allowedPermissions.includes(permission)) { @@ -307,6 +349,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..887edc6 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 @@ -519,6 +533,7 @@ function App() { themeColor={themeColor} textColor={textColor} showTabOverview={showTabOverview} + isFullscreen={isFullscreen} tabOverviewContent={ - {/* 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..3c55db9 100644 --- a/apps/browser/src/types/electron-api.d.ts +++ b/apps/browser/src/types/electron-api.d.ts @@ -48,6 +48,9 @@ interface ElectronAPI { toggleOrientation: () => Promise; onOrientationChanged: (callback: (orientation: 'portrait' | 'landscape') => void) => () => void; + // Fullscreen mode listener + onFullscreenModeChanged: (callback: (isFullscreen: boolean) => void) => () => void; + // Tab management APIs tabs: { getAll: () => Promise; diff --git a/apps/browser/src/webview-preload.ts b/apps/browser/src/webview-preload.ts index afefe75..75d84bc 100644 --- a/apps/browser/src/webview-preload.ts +++ b/apps/browser/src/webview-preload.ts @@ -3,6 +3,58 @@ 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; + +// 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 + const event = new Event("fullscreenchange", { bubbles: true }); + document.dispatchEvent(event); + } else if (!state && wasFullscreen) { + // Exiting fullscreen + fullscreenElement = null; + 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, +}); + +console.log("[Preload] ✓ Fullscreen API polyfill installed"); + +// ============================================================================ +// Theme Color Extraction +// ============================================================================ + // Extract theme color safely when DOM is ready function extractThemeColor(): string | null { try { From e662ee81077c2d87b1583e642502d78e2e979b8d Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 22:16:14 +0900 Subject: [PATCH 02/10] fix: improve fullscreen layout with dynamic gaps and screen size overrides --- apps/browser/src/main/tab-manager.ts | 230 ++++++++++++++++++++------- apps/browser/src/webview-preload.ts | 70 +++++++- 2 files changed, 241 insertions(+), 59 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index 7513436..af51943 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 { @@ -91,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) => { @@ -237,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 @@ -288,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); @@ -320,14 +333,18 @@ export class TabManager { * 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 { + 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; - console.log("[Fullscreen] enter-html-full-screen event received"); - + 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; @@ -335,79 +352,163 @@ export class TabManager { if (this.state.mainWindow) { const windowBounds = this.state.mainWindow.getBounds(); const topBarHeight = 40; // TOP_BAR_HEIGHT - const fullscreenGap = 50; // 50px gap - - if (this.state.isLandscape) { - // Landscape: 50px gap on left and right - tab.view.setBounds({ - x: fullscreenGap, - y: topBarHeight, - width: windowBounds.width - (fullscreenGap * 2), - height: windowBounds.height - topBarHeight, - }); + 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; + + const ts = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}`); + console.log(`[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? 'LANDSCAPE' : 'PORTRAIT'}`); + console.log(`[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px`); + + 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, + y: topBarHeight + deviceFramePadding, + width: windowBounds.width - fullscreenGapHorizontal * 2, + height: windowBounds.height - topBarHeight - deviceFramePadding * 2, + }; + console.log(`[Fullscreen][${ts}] LANDSCAPE bounds:`, bounds); + tab.view.setBounds(bounds); } else { - // Portrait: 50px gap on top and bottom - tab.view.setBounds({ - x: 0, - y: topBarHeight + fullscreenGap, - width: windowBounds.width, - height: windowBounds.height - topBarHeight - (fullscreenGap * 2), - }); + // Portrait: gap on top and bottom to avoid rounded corners + const bounds = { + x: deviceFramePadding, + y: topBarHeight + fullscreenGapVertical, + width: windowBounds.width - deviceFramePadding * 2, + height: + windowBounds.height - + topBarHeight - + fullscreenGapVertical - + fullscreenGapVertical, + }; + console.log(`[Fullscreen][${ts}] PORTRAIT bounds:`, bounds); + 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(); + const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + this.state.mainWindow.setBounds({ + ...windowBoundsNow, + height: windowBoundsNow.height + 1, + }); + + // Immediately restore to correct size + const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + this.state.mainWindow.setBounds(windowBoundsNow); - // Notify webview that it's in fullscreen state (for CSS/JS) - tab.view.webContents.send("set-fullscreen-state", true); + // Send fullscreen state immediately + if (!tab.view.webContents.isDestroyed()) { + const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: true`); + tab.view.webContents.send("set-fullscreen-state", true); + } } - console.log("[Fullscreen] ✅ Fullscreen mode enabled (with gaps, status bar hidden)"); + const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log( + `[Fullscreen][${ts2}] ✅ Fullscreen mode enabled (with gaps, status bar hidden)` + ); }); contents.on("leave-html-full-screen", () => { const tab = this.state.tabs.find((t) => t.id === tabId); if (!tab) return; - console.log("[Fullscreen] leave-html-full-screen event received"); + const timestamp = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${timestamp}] leave-html-full-screen event received`); // Clear fullscreen state tab.isFullscreen = false; // Restore normal bounds if (this.state.mainWindow) { - this.state.mainWindow.webContents.send("fullscreen-mode-changed", false); - - // Notify webview that it's no longer in fullscreen state - tab.view.webContents.send("set-fullscreen-state", false); + this.state.mainWindow.webContents.send( + "fullscreen-mode-changed", + false + ); - // Restore normal WebContentsView bounds + // Restore normal WebContentsView bounds FIRST const windowBounds = this.state.mainWindow.getBounds(); const topBarHeight = 40; // TOP_BAR_HEIGHT const statusBarHeight = 58; const statusBarWidth = 58; - const framePadding = 15; // Device frame padding - - if (this.state.isLandscape) { - // Landscape mode - tab.view.setBounds({ - x: statusBarWidth + framePadding, - y: topBarHeight + framePadding, - width: windowBounds.width - statusBarWidth - framePadding * 2, - height: windowBounds.height - topBarHeight - framePadding * 2, - }); + 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), + }; + const ts = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, bounds); + tab.view.setBounds(bounds); } else { - // Portrait mode - tab.view.setBounds({ - x: framePadding, - y: topBarHeight + statusBarHeight + framePadding, - width: windowBounds.width - framePadding * 2, - height: windowBounds.height - topBarHeight - statusBarHeight - framePadding * 2, - }); + // 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), + }; + const ts = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts}] Restoring PORTRAIT bounds:`, bounds); + tab.view.setBounds(bounds); + } + + // Force a layout recalculation by resizing the main window + // This ensures WebContentsView properly recalculates its size + const windowBoundsNow = this.state.mainWindow.getBounds(); + const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + this.state.mainWindow.setBounds({ + ...windowBoundsNow, + height: windowBoundsNow.height + 1, + }); + + // Immediately restore to correct size + const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + this.state.mainWindow.setBounds(windowBoundsNow); + + // Send fullscreen state immediately + if (!tab.view.webContents.isDestroyed()) { + const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: false`); + tab.view.webContents.send("set-fullscreen-state", false); } } - console.log("[Fullscreen] ✅ Fullscreen state cleared (status bar shown, bounds restored)"); + const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log( + `[Fullscreen][${ts2}] ✅ Fullscreen state cleared (status bar shown, bounds restored)` + ); }); } @@ -421,7 +522,9 @@ export class TabManager { console.log("[Fullscreen] Exiting fullscreen via ESC key"); // Execute JavaScript to exit fullscreen in the web page - tab.view.webContents.executeJavaScript(` + tab.view.webContents + .executeJavaScript( + ` if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { @@ -431,9 +534,11 @@ export class TabManager { } else if (document.msExitFullscreen) { document.msExitFullscreen(); } - `).catch((err) => { - console.error("[Fullscreen] Failed to exit fullscreen:", err); - }); + ` + ) + .catch((err) => { + console.error("[Fullscreen] Failed to exit fullscreen:", err); + }); // Notify webview-preload to update state if (!tab.view.webContents.isDestroyed()) { @@ -444,7 +549,10 @@ export class TabManager { /** * 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(); @@ -509,7 +617,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", { @@ -540,7 +651,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/webview-preload.ts b/apps/browser/src/webview-preload.ts index 75d84bc..10af795 100644 --- a/apps/browser/src/webview-preload.ts +++ b/apps/browser/src/webview-preload.ts @@ -20,16 +20,36 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { const wasFullscreen = isFullscreenActive; isFullscreenActive = state; + const timestamp = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Preload][${timestamp}] Fullscreen state changed: ${state}`); + console.log(`[Preload][${timestamp}] Window size: ${window.innerWidth}x${window.innerHeight}`); + console.log(`[Preload][${timestamp}] Screen size (overridden): ${window.screen.width}x${window.screen.height}`); + console.log(`[Preload][${timestamp}] Original screen size: ${originalScreenWidth}x${originalScreenHeight}`); + if (state && !wasFullscreen) { // Entering fullscreen fullscreenElement = document.documentElement; // Assume whole document + + // 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); + + const ts1 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Preload][${ts1}] ✅ Entered fullscreen mode`); } else if (!state && wasFullscreen) { // Exiting fullscreen fullscreenElement = null; + + // Force a resize event + window.dispatchEvent(new Event('resize')); + const event = new Event("fullscreenchange", { bubbles: true }); document.dispatchEvent(event); + + const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Preload][${ts2}] ✅ Exited fullscreen mode`); } }); @@ -49,7 +69,55 @@ Object.defineProperty(Document.prototype, "webkitFullscreenElement", { configurable: true, }); -console.log("[Preload] ✓ Fullscreen API polyfill installed"); +// 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 From d2f8d432bd06665849e101d94d49b8cf1da62d76 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 22:16:32 +0900 Subject: [PATCH 03/10] style: format code with consistent double quotes and line breaks --- apps/browser/src/main/tab-manager.ts | 94 +++++++++++++++++++--------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index af51943..d539d97 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -342,8 +342,10 @@ export class TabManager { 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`); + 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; @@ -367,10 +369,16 @@ export class TabManager { // Determine orientation based on actual window dimensions (not cached state) const isCurrentlyLandscape = windowBounds.width > windowBounds.height; - const ts = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}`); - console.log(`[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? 'LANDSCAPE' : 'PORTRAIT'}`); - console.log(`[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px`); + const ts = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}` + ); + console.log( + `[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? "LANDSCAPE" : "PORTRAIT"}` + ); + console.log( + `[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px` + ); if (isCurrentlyLandscape) { // Landscape: gap on left and right to avoid rounded corners @@ -405,27 +413,33 @@ export class TabManager { // Force a layout recalculation by resizing the main window // This ensures WebContentsView properly recalculates its size const windowBoundsNow = this.state.mainWindow.getBounds(); - const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}` + ); this.state.mainWindow.setBounds({ ...windowBoundsNow, height: windowBoundsNow.height + 1, }); - + // Immediately restore to correct size - const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` + ); this.state.mainWindow.setBounds(windowBoundsNow); - + // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { - const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: true`); + const ts5 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts5}] Sending set-fullscreen-state: true` + ); tab.view.webContents.send("set-fullscreen-state", true); } } - const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + const ts2 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( `[Fullscreen][${ts2}] ✅ Fullscreen mode enabled (with gaps, status bar hidden)` ); @@ -435,8 +449,10 @@ export class TabManager { 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}] leave-html-full-screen event received`); + const timestamp = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${timestamp}] leave-html-full-screen event received` + ); // Clear fullscreen state tab.isFullscreen = false; @@ -464,10 +480,15 @@ export class TabManager { x: statusBarWidth, y: Math.round(topBarHeight + frameHalf), width: Math.round(windowBounds.width - statusBarWidth - frameHalf), - height: Math.round(windowBounds.height - topBarHeight - frameHalf * 2), + height: Math.round( + windowBounds.height - topBarHeight - frameHalf * 2 + ), }; - const ts = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, bounds); + const ts = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, + bounds + ); tab.view.setBounds(bounds); } else { // Portrait mode: status bar is on the TOP @@ -475,9 +496,14 @@ export class TabManager { 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), + height: Math.round( + windowBounds.height - + topBarHeight - + statusBarHeight - + frameHalf * 2 + ), }; - const ts = new Date().toISOString().split('T')[1].slice(0, -1); + const ts = new Date().toISOString().split("T")[1].slice(0, -1); console.log(`[Fullscreen][${ts}] Restoring PORTRAIT bounds:`, bounds); tab.view.setBounds(bounds); } @@ -485,27 +511,33 @@ export class TabManager { // Force a layout recalculation by resizing the main window // This ensures WebContentsView properly recalculates its size const windowBoundsNow = this.state.mainWindow.getBounds(); - const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}` + ); this.state.mainWindow.setBounds({ ...windowBoundsNow, height: windowBoundsNow.height + 1, }); - + // Immediately restore to correct size - const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` + ); this.state.mainWindow.setBounds(windowBoundsNow); - + // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { - const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: false`); + const ts5 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts5}] Sending set-fullscreen-state: false` + ); tab.view.webContents.send("set-fullscreen-state", false); } } - const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + const ts2 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( `[Fullscreen][${ts2}] ✅ Fullscreen state cleared (status bar shown, bounds restored)` ); From 54a75150613638f9b5992927ef5172f4d76e2584 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 22:16:14 +0900 Subject: [PATCH 04/10] fix: improve fullscreen layout with dynamic gaps and screen size overrides #70 --- apps/browser/src/main/tab-manager.ts | 230 ++++++++++++++++++++------- apps/browser/src/webview-preload.ts | 70 +++++++- 2 files changed, 241 insertions(+), 59 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index 7513436..af51943 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 { @@ -91,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) => { @@ -237,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 @@ -288,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); @@ -320,14 +333,18 @@ export class TabManager { * 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 { + 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; - console.log("[Fullscreen] enter-html-full-screen event received"); - + 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; @@ -335,79 +352,163 @@ export class TabManager { if (this.state.mainWindow) { const windowBounds = this.state.mainWindow.getBounds(); const topBarHeight = 40; // TOP_BAR_HEIGHT - const fullscreenGap = 50; // 50px gap - - if (this.state.isLandscape) { - // Landscape: 50px gap on left and right - tab.view.setBounds({ - x: fullscreenGap, - y: topBarHeight, - width: windowBounds.width - (fullscreenGap * 2), - height: windowBounds.height - topBarHeight, - }); + 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; + + const ts = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}`); + console.log(`[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? 'LANDSCAPE' : 'PORTRAIT'}`); + console.log(`[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px`); + + 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, + y: topBarHeight + deviceFramePadding, + width: windowBounds.width - fullscreenGapHorizontal * 2, + height: windowBounds.height - topBarHeight - deviceFramePadding * 2, + }; + console.log(`[Fullscreen][${ts}] LANDSCAPE bounds:`, bounds); + tab.view.setBounds(bounds); } else { - // Portrait: 50px gap on top and bottom - tab.view.setBounds({ - x: 0, - y: topBarHeight + fullscreenGap, - width: windowBounds.width, - height: windowBounds.height - topBarHeight - (fullscreenGap * 2), - }); + // Portrait: gap on top and bottom to avoid rounded corners + const bounds = { + x: deviceFramePadding, + y: topBarHeight + fullscreenGapVertical, + width: windowBounds.width - deviceFramePadding * 2, + height: + windowBounds.height - + topBarHeight - + fullscreenGapVertical - + fullscreenGapVertical, + }; + console.log(`[Fullscreen][${ts}] PORTRAIT bounds:`, bounds); + 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(); + const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + this.state.mainWindow.setBounds({ + ...windowBoundsNow, + height: windowBoundsNow.height + 1, + }); + + // Immediately restore to correct size + const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + this.state.mainWindow.setBounds(windowBoundsNow); - // Notify webview that it's in fullscreen state (for CSS/JS) - tab.view.webContents.send("set-fullscreen-state", true); + // Send fullscreen state immediately + if (!tab.view.webContents.isDestroyed()) { + const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: true`); + tab.view.webContents.send("set-fullscreen-state", true); + } } - console.log("[Fullscreen] ✅ Fullscreen mode enabled (with gaps, status bar hidden)"); + const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log( + `[Fullscreen][${ts2}] ✅ Fullscreen mode enabled (with gaps, status bar hidden)` + ); }); contents.on("leave-html-full-screen", () => { const tab = this.state.tabs.find((t) => t.id === tabId); if (!tab) return; - console.log("[Fullscreen] leave-html-full-screen event received"); + const timestamp = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${timestamp}] leave-html-full-screen event received`); // Clear fullscreen state tab.isFullscreen = false; // Restore normal bounds if (this.state.mainWindow) { - this.state.mainWindow.webContents.send("fullscreen-mode-changed", false); - - // Notify webview that it's no longer in fullscreen state - tab.view.webContents.send("set-fullscreen-state", false); + this.state.mainWindow.webContents.send( + "fullscreen-mode-changed", + false + ); - // Restore normal WebContentsView bounds + // Restore normal WebContentsView bounds FIRST const windowBounds = this.state.mainWindow.getBounds(); const topBarHeight = 40; // TOP_BAR_HEIGHT const statusBarHeight = 58; const statusBarWidth = 58; - const framePadding = 15; // Device frame padding - - if (this.state.isLandscape) { - // Landscape mode - tab.view.setBounds({ - x: statusBarWidth + framePadding, - y: topBarHeight + framePadding, - width: windowBounds.width - statusBarWidth - framePadding * 2, - height: windowBounds.height - topBarHeight - framePadding * 2, - }); + 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), + }; + const ts = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, bounds); + tab.view.setBounds(bounds); } else { - // Portrait mode - tab.view.setBounds({ - x: framePadding, - y: topBarHeight + statusBarHeight + framePadding, - width: windowBounds.width - framePadding * 2, - height: windowBounds.height - topBarHeight - statusBarHeight - framePadding * 2, - }); + // 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), + }; + const ts = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts}] Restoring PORTRAIT bounds:`, bounds); + tab.view.setBounds(bounds); + } + + // Force a layout recalculation by resizing the main window + // This ensures WebContentsView properly recalculates its size + const windowBoundsNow = this.state.mainWindow.getBounds(); + const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + this.state.mainWindow.setBounds({ + ...windowBoundsNow, + height: windowBoundsNow.height + 1, + }); + + // Immediately restore to correct size + const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + this.state.mainWindow.setBounds(windowBoundsNow); + + // Send fullscreen state immediately + if (!tab.view.webContents.isDestroyed()) { + const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: false`); + tab.view.webContents.send("set-fullscreen-state", false); } } - console.log("[Fullscreen] ✅ Fullscreen state cleared (status bar shown, bounds restored)"); + const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log( + `[Fullscreen][${ts2}] ✅ Fullscreen state cleared (status bar shown, bounds restored)` + ); }); } @@ -421,7 +522,9 @@ export class TabManager { console.log("[Fullscreen] Exiting fullscreen via ESC key"); // Execute JavaScript to exit fullscreen in the web page - tab.view.webContents.executeJavaScript(` + tab.view.webContents + .executeJavaScript( + ` if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { @@ -431,9 +534,11 @@ export class TabManager { } else if (document.msExitFullscreen) { document.msExitFullscreen(); } - `).catch((err) => { - console.error("[Fullscreen] Failed to exit fullscreen:", err); - }); + ` + ) + .catch((err) => { + console.error("[Fullscreen] Failed to exit fullscreen:", err); + }); // Notify webview-preload to update state if (!tab.view.webContents.isDestroyed()) { @@ -444,7 +549,10 @@ export class TabManager { /** * 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(); @@ -509,7 +617,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", { @@ -540,7 +651,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/webview-preload.ts b/apps/browser/src/webview-preload.ts index 75d84bc..10af795 100644 --- a/apps/browser/src/webview-preload.ts +++ b/apps/browser/src/webview-preload.ts @@ -20,16 +20,36 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { const wasFullscreen = isFullscreenActive; isFullscreenActive = state; + const timestamp = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Preload][${timestamp}] Fullscreen state changed: ${state}`); + console.log(`[Preload][${timestamp}] Window size: ${window.innerWidth}x${window.innerHeight}`); + console.log(`[Preload][${timestamp}] Screen size (overridden): ${window.screen.width}x${window.screen.height}`); + console.log(`[Preload][${timestamp}] Original screen size: ${originalScreenWidth}x${originalScreenHeight}`); + if (state && !wasFullscreen) { // Entering fullscreen fullscreenElement = document.documentElement; // Assume whole document + + // 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); + + const ts1 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Preload][${ts1}] ✅ Entered fullscreen mode`); } else if (!state && wasFullscreen) { // Exiting fullscreen fullscreenElement = null; + + // Force a resize event + window.dispatchEvent(new Event('resize')); + const event = new Event("fullscreenchange", { bubbles: true }); document.dispatchEvent(event); + + const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + console.log(`[Preload][${ts2}] ✅ Exited fullscreen mode`); } }); @@ -49,7 +69,55 @@ Object.defineProperty(Document.prototype, "webkitFullscreenElement", { configurable: true, }); -console.log("[Preload] ✓ Fullscreen API polyfill installed"); +// 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 From 91f86039c88097f7c9ee88a4d776b201a49c5572 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 22:16:32 +0900 Subject: [PATCH 05/10] style: format code with consistent double quotes and line breaks #70 --- apps/browser/src/main/tab-manager.ts | 94 +++++++++++++++++++--------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index af51943..d539d97 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -342,8 +342,10 @@ export class TabManager { 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`); + 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; @@ -367,10 +369,16 @@ export class TabManager { // Determine orientation based on actual window dimensions (not cached state) const isCurrentlyLandscape = windowBounds.width > windowBounds.height; - const ts = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}`); - console.log(`[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? 'LANDSCAPE' : 'PORTRAIT'}`); - console.log(`[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px`); + const ts = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}` + ); + console.log( + `[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? "LANDSCAPE" : "PORTRAIT"}` + ); + console.log( + `[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px` + ); if (isCurrentlyLandscape) { // Landscape: gap on left and right to avoid rounded corners @@ -405,27 +413,33 @@ export class TabManager { // Force a layout recalculation by resizing the main window // This ensures WebContentsView properly recalculates its size const windowBoundsNow = this.state.mainWindow.getBounds(); - const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}` + ); this.state.mainWindow.setBounds({ ...windowBoundsNow, height: windowBoundsNow.height + 1, }); - + // Immediately restore to correct size - const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` + ); this.state.mainWindow.setBounds(windowBoundsNow); - + // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { - const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: true`); + const ts5 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts5}] Sending set-fullscreen-state: true` + ); tab.view.webContents.send("set-fullscreen-state", true); } } - const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + const ts2 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( `[Fullscreen][${ts2}] ✅ Fullscreen mode enabled (with gaps, status bar hidden)` ); @@ -435,8 +449,10 @@ export class TabManager { 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}] leave-html-full-screen event received`); + const timestamp = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${timestamp}] leave-html-full-screen event received` + ); // Clear fullscreen state tab.isFullscreen = false; @@ -464,10 +480,15 @@ export class TabManager { x: statusBarWidth, y: Math.round(topBarHeight + frameHalf), width: Math.round(windowBounds.width - statusBarWidth - frameHalf), - height: Math.round(windowBounds.height - topBarHeight - frameHalf * 2), + height: Math.round( + windowBounds.height - topBarHeight - frameHalf * 2 + ), }; - const ts = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, bounds); + const ts = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, + bounds + ); tab.view.setBounds(bounds); } else { // Portrait mode: status bar is on the TOP @@ -475,9 +496,14 @@ export class TabManager { 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), + height: Math.round( + windowBounds.height - + topBarHeight - + statusBarHeight - + frameHalf * 2 + ), }; - const ts = new Date().toISOString().split('T')[1].slice(0, -1); + const ts = new Date().toISOString().split("T")[1].slice(0, -1); console.log(`[Fullscreen][${ts}] Restoring PORTRAIT bounds:`, bounds); tab.view.setBounds(bounds); } @@ -485,27 +511,33 @@ export class TabManager { // Force a layout recalculation by resizing the main window // This ensures WebContentsView properly recalculates its size const windowBoundsNow = this.state.mainWindow.getBounds(); - const ts3 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}`); + const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}` + ); this.state.mainWindow.setBounds({ ...windowBoundsNow, height: windowBoundsNow.height + 1, }); - + // Immediately restore to correct size - const ts4 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}`); + const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` + ); this.state.mainWindow.setBounds(windowBoundsNow); - + // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { - const ts5 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Fullscreen][${ts5}] Sending set-fullscreen-state: false`); + const ts5 = new Date().toISOString().split("T")[1].slice(0, -1); + console.log( + `[Fullscreen][${ts5}] Sending set-fullscreen-state: false` + ); tab.view.webContents.send("set-fullscreen-state", false); } } - const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); + const ts2 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( `[Fullscreen][${ts2}] ✅ Fullscreen state cleared (status bar shown, bounds restored)` ); From f5184ca155fb7c4ec311067f67895538c0a26f34 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 23:06:33 +0900 Subject: [PATCH 06/10] fix: adjust web content bounds in fullscreen mode to reduce gaps by 30px --- apps/browser/src/main/tab-manager.ts | 62 +++++++++++++++++-- apps/browser/src/main/window-manager.ts | 43 ++++++++----- apps/browser/src/renderer/app.tsx | 43 ++++++------- .../src/renderer/components/phone-frame.tsx | 27 +++++--- 4 files changed, 125 insertions(+), 50 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index d539d97..5644252 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -384,7 +384,7 @@ export class TabManager { // Landscape: gap on left and right to avoid rounded corners // Note: We ignore status bar space in fullscreen mode const bounds = { - x: fullscreenGapHorizontal, + x: fullscreenGapHorizontal - 30, y: topBarHeight + deviceFramePadding, width: windowBounds.width - fullscreenGapHorizontal * 2, height: windowBounds.height - topBarHeight - deviceFramePadding * 2, @@ -395,7 +395,7 @@ export class TabManager { // Portrait: gap on top and bottom to avoid rounded corners const bounds = { x: deviceFramePadding, - y: topBarHeight + fullscreenGapVertical, + y: topBarHeight + fullscreenGapVertical - 30, width: windowBounds.width - deviceFramePadding * 2, height: windowBounds.height - @@ -422,12 +422,37 @@ export class TabManager { height: windowBoundsNow.height + 1, }); - // Immediately restore to correct size + // Immediately restore to correct size and reapply adjusted bounds const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` ); 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, + }; + console.log(`[Fullscreen][${ts4}] Reapplying LANDSCAPE bounds:`, adjustedBounds); + tab.view.setBounds(adjustedBounds); + } else { + const adjustedBounds = { + x: deviceFramePadding, + y: topBarHeight + fullscreenGapVertical - 30, + width: windowBounds.width - deviceFramePadding * 2, + height: + windowBounds.height - + topBarHeight - + fullscreenGapVertical - + fullscreenGapVertical, + }; + console.log(`[Fullscreen][${ts4}] Reapplying PORTRAIT bounds:`, adjustedBounds); + tab.view.setBounds(adjustedBounds); + } // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { @@ -509,7 +534,6 @@ export class TabManager { } // Force a layout recalculation by resizing the main window - // This ensures WebContentsView properly recalculates its size const windowBoundsNow = this.state.mainWindow.getBounds(); const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( @@ -520,12 +544,40 @@ export class TabManager { height: windowBoundsNow.height + 1, }); - // Immediately restore to correct size + // Immediately restore to correct size and reapply adjusted bounds const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); console.log( `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` ); 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 + ), + }; + console.log(`[Fullscreen][${ts4}] Reapplying LANDSCAPE bounds:`, adjustedBounds); + 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 + ), + }; + console.log(`[Fullscreen][${ts4}] Reapplying PORTRAIT bounds:`, adjustedBounds); + tab.view.setBounds(adjustedBounds); + } // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { diff --git a/apps/browser/src/main/window-manager.ts b/apps/browser/src/main/window-manager.ts index 58acb0b..29ebc3f 100644 --- a/apps/browser/src/main/window-manager.ts +++ b/apps/browser/src/main/window-manager.ts @@ -54,24 +54,35 @@ export class WindowManager { // 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 fullscreenGap = 50; // 50px gap for fullscreen mode - + const deviceFramePadding = FRAME_PADDING / 2; + const fullscreenGapHorizontal = 57; // Match tab-manager + const fullscreenGapVertical = 67; // Match tab-manager + + const ts = new Date().toISOString().split("T")[1].slice(0, -1); + console.log(`[WindowManager][${ts}] updateWebContentsViewBounds called in FULLSCREEN mode`); + console.log(`[WindowManager][${ts}] Window bounds:`, windowBounds); + console.log(`[WindowManager][${ts}] Orientation: ${this.state.isLandscape ? 'LANDSCAPE' : 'PORTRAIT'}`); + if (this.state.isLandscape) { - // Landscape: 50px gap on left and right - activeTab.view.setBounds({ - x: fullscreenGap, - y: topBarHeight, - width: windowBounds.width - (fullscreenGap * 2), - height: windowBounds.height - topBarHeight, - }); + // 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, + }; + console.log(`[WindowManager][${ts}] Setting LANDSCAPE bounds:`, bounds); + activeTab.view.setBounds(bounds); } else { - // Portrait: 50px gap on top and bottom - activeTab.view.setBounds({ - x: 0, - y: topBarHeight + fullscreenGap, - width: windowBounds.width, - height: windowBounds.height - topBarHeight - (fullscreenGap * 2), - }); + // 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, + }; + console.log(`[WindowManager][${ts}] Setting PORTRAIT bounds:`, bounds); + activeTab.view.setBounds(bounds); } // Notify renderer to hide status bar in fullscreen mode diff --git a/apps/browser/src/renderer/app.tsx b/apps/browser/src/renderer/app.tsx index 887edc6..56f6a34 100644 --- a/apps/browser/src/renderer/app.tsx +++ b/apps/browser/src/renderer/app.tsx @@ -87,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) + // ), + // }); + // } } ); diff --git a/apps/browser/src/renderer/components/phone-frame.tsx b/apps/browser/src/renderer/components/phone-frame.tsx index 557cccf..b30613a 100644 --- a/apps/browser/src/renderer/components/phone-frame.tsx +++ b/apps/browser/src/renderer/components/phone-frame.tsx @@ -33,13 +33,24 @@ function PhoneFrame({ const statusBarHeight = 58; const statusBarWidth = 58; - // Web content positioned below/beside status bar - window.electronAPI?.webContents.setBounds({ - x: Math.round(rect.x + (isLandscape ? statusBarWidth : 0)), - y: Math.round(rect.y + (isLandscape ? 0 : statusBarHeight)), - width: Math.round(rect.width - (isLandscape ? statusBarWidth : 0)), - height: Math.round(rect.height - (isLandscape ? 0 : statusBarHeight)), - }); + // In fullscreen mode, apply -30px offset + if (isFullscreen) { + // Fullscreen mode: apply offset to move content closer to edges + window.electronAPI?.webContents.setBounds({ + x: Math.round(rect.x + (isLandscape ? statusBarWidth - 30 : 0)), + y: Math.round(rect.y + (isLandscape ? 0 : statusBarHeight - 30)), + width: Math.round(rect.width - (isLandscape ? statusBarWidth : 0)), + height: Math.round(rect.height - (isLandscape ? 0 : statusBarHeight)), + }); + } else { + // Normal mode: standard positioning + window.electronAPI?.webContents.setBounds({ + x: Math.round(rect.x + (isLandscape ? statusBarWidth : 0)), + y: Math.round(rect.y + (isLandscape ? 0 : statusBarHeight)), + width: Math.round(rect.width - (isLandscape ? statusBarWidth : 0)), + height: Math.round(rect.height - (isLandscape ? 0 : statusBarHeight)), + }); + } }; // Initial bounds update with multiple attempts to ensure it's set @@ -53,7 +64,7 @@ function PhoneFrame({ return () => { window.removeEventListener("resize", updateBounds); }; - }, [webContainerRef, orientation]); + }, [webContainerRef, orientation, isFullscreen]); return (
Date: Sun, 19 Oct 2025 23:14:17 +0900 Subject: [PATCH 07/10] feat: add video scaling fix for fullscreen mode using object-fit contain --- apps/browser/src/webview-preload.ts | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/browser/src/webview-preload.ts b/apps/browser/src/webview-preload.ts index 10af795..bacba7d 100644 --- a/apps/browser/src/webview-preload.ts +++ b/apps/browser/src/webview-preload.ts @@ -15,6 +15,59 @@ console.log("[Preload] ✓ Loaded - Fullscreen API polyfill active"); 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; @@ -30,6 +83,9 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { // 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')); @@ -42,6 +98,9 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { // Exiting fullscreen fullscreenElement = null; + // Restore original object-fit style + applyVideoFitStyle(''); + // Force a resize event window.dispatchEvent(new Event('resize')); From 72bc75e269eb47d91edadb9c9503b82cd1b19df5 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 23:16:07 +0900 Subject: [PATCH 08/10] chore: export ElectronAPI interface and format code with prettier --- apps/browser/src/types/electron-api.d.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/apps/browser/src/types/electron-api.d.ts b/apps/browser/src/types/electron-api.d.ts index 3c55db9..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,16 +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; + onFullscreenModeChanged: ( + callback: (isFullscreen: boolean) => void + ) => () => void; // Tab management APIs tabs: { @@ -84,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; }; } From a6a0e0675d21ad93a67043e1f4589bbe65f98479 Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 23:19:59 +0900 Subject: [PATCH 09/10] style: update phone frame background color from transparent to black --- apps/browser/src/renderer/components/phone-frame.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/browser/src/renderer/components/phone-frame.tsx b/apps/browser/src/renderer/components/phone-frame.tsx index b30613a..4d85751 100644 --- a/apps/browser/src/renderer/components/phone-frame.tsx +++ b/apps/browser/src/renderer/components/phone-frame.tsx @@ -78,11 +78,11 @@ function PhoneFrame({ {/* Device frame - visual only, clicks pass through */}
-
+
{/* Web content area - positioned for WebContentsView */}
{/* Status bar - React component on top (hidden in fullscreen) */} {!isFullscreen && ( From b6bbcc9602f8e9d5cd5a867f3d7b446aa10185ec Mon Sep 17 00:00:00 2001 From: hmmhmmhm Date: Sun, 19 Oct 2025 23:26:24 +0900 Subject: [PATCH 10/10] chore: remove debug logging statements from fullscreen mode implementation --- apps/browser/src/main/tab-manager.ts | 63 ------------------------- apps/browser/src/main/window-manager.ts | 7 --- apps/browser/src/webview-preload.ts | 12 ----- 3 files changed, 82 deletions(-) diff --git a/apps/browser/src/main/tab-manager.ts b/apps/browser/src/main/tab-manager.ts index 5644252..17ac221 100644 --- a/apps/browser/src/main/tab-manager.ts +++ b/apps/browser/src/main/tab-manager.ts @@ -369,17 +369,6 @@ export class TabManager { // Determine orientation based on actual window dimensions (not cached state) const isCurrentlyLandscape = windowBounds.width > windowBounds.height; - const ts = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts}] Window bounds: ${windowBounds.width}x${windowBounds.height}` - ); - console.log( - `[Fullscreen][${ts}] Orientation: ${isCurrentlyLandscape ? "LANDSCAPE" : "PORTRAIT"}` - ); - console.log( - `[Fullscreen][${ts}] Gaps - Vertical: ${fullscreenGapVertical}px, Horizontal: ${fullscreenGapHorizontal}px` - ); - if (isCurrentlyLandscape) { // Landscape: gap on left and right to avoid rounded corners // Note: We ignore status bar space in fullscreen mode @@ -389,7 +378,6 @@ export class TabManager { width: windowBounds.width - fullscreenGapHorizontal * 2, height: windowBounds.height - topBarHeight - deviceFramePadding * 2, }; - console.log(`[Fullscreen][${ts}] LANDSCAPE bounds:`, bounds); tab.view.setBounds(bounds); } else { // Portrait: gap on top and bottom to avoid rounded corners @@ -403,7 +391,6 @@ export class TabManager { fullscreenGapVertical - fullscreenGapVertical, }; - console.log(`[Fullscreen][${ts}] PORTRAIT bounds:`, bounds); tab.view.setBounds(bounds); } @@ -413,20 +400,12 @@ export class TabManager { // Force a layout recalculation by resizing the main window // This ensures WebContentsView properly recalculates its size const windowBoundsNow = this.state.mainWindow.getBounds(); - const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}` - ); this.state.mainWindow.setBounds({ ...windowBoundsNow, height: windowBoundsNow.height + 1, }); // Immediately restore to correct size and reapply adjusted bounds - const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` - ); this.state.mainWindow.setBounds(windowBoundsNow); // Reapply the adjusted bounds after window resize @@ -437,7 +416,6 @@ export class TabManager { width: windowBounds.width - fullscreenGapHorizontal * 2, height: windowBounds.height - topBarHeight - deviceFramePadding * 2, }; - console.log(`[Fullscreen][${ts4}] Reapplying LANDSCAPE bounds:`, adjustedBounds); tab.view.setBounds(adjustedBounds); } else { const adjustedBounds = { @@ -450,35 +428,21 @@ export class TabManager { fullscreenGapVertical - fullscreenGapVertical, }; - console.log(`[Fullscreen][${ts4}] Reapplying PORTRAIT bounds:`, adjustedBounds); tab.view.setBounds(adjustedBounds); } // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { - const ts5 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts5}] Sending set-fullscreen-state: true` - ); tab.view.webContents.send("set-fullscreen-state", true); } } - const ts2 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts2}] ✅ Fullscreen mode enabled (with gaps, status bar hidden)` - ); }); contents.on("leave-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}] leave-html-full-screen event received` - ); - // Clear fullscreen state tab.isFullscreen = false; @@ -509,11 +473,6 @@ export class TabManager { windowBounds.height - topBarHeight - frameHalf * 2 ), }; - const ts = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts}] Restoring LANDSCAPE bounds:`, - bounds - ); tab.view.setBounds(bounds); } else { // Portrait mode: status bar is on the TOP @@ -528,27 +487,17 @@ export class TabManager { frameHalf * 2 ), }; - const ts = new Date().toISOString().split("T")[1].slice(0, -1); - console.log(`[Fullscreen][${ts}] Restoring PORTRAIT bounds:`, bounds); tab.view.setBounds(bounds); } // Force a layout recalculation by resizing the main window const windowBoundsNow = this.state.mainWindow.getBounds(); - const ts3 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts3}] Applying window resize trick: ${windowBoundsNow.height} -> ${windowBoundsNow.height + 1}` - ); this.state.mainWindow.setBounds({ ...windowBoundsNow, height: windowBoundsNow.height + 1, }); // Immediately restore to correct size and reapply adjusted bounds - const ts4 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts4}] Restoring window to correct size: ${windowBoundsNow.height}` - ); this.state.mainWindow.setBounds(windowBoundsNow); // Reapply the adjusted bounds after window resize @@ -561,7 +510,6 @@ export class TabManager { windowBounds.height - topBarHeight - frameHalf * 2 ), }; - console.log(`[Fullscreen][${ts4}] Reapplying LANDSCAPE bounds:`, adjustedBounds); tab.view.setBounds(adjustedBounds); } else { const adjustedBounds = { @@ -575,24 +523,15 @@ export class TabManager { frameHalf * 2 ), }; - console.log(`[Fullscreen][${ts4}] Reapplying PORTRAIT bounds:`, adjustedBounds); tab.view.setBounds(adjustedBounds); } // Send fullscreen state immediately if (!tab.view.webContents.isDestroyed()) { - const ts5 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts5}] Sending set-fullscreen-state: false` - ); tab.view.webContents.send("set-fullscreen-state", false); } } - const ts2 = new Date().toISOString().split("T")[1].slice(0, -1); - console.log( - `[Fullscreen][${ts2}] ✅ Fullscreen state cleared (status bar shown, bounds restored)` - ); }); } @@ -603,8 +542,6 @@ export class TabManager { const tab = this.state.tabs.find((t) => t.id === tabId); if (!tab || !tab.isFullscreen) return; - console.log("[Fullscreen] Exiting fullscreen via ESC key"); - // Execute JavaScript to exit fullscreen in the web page tab.view.webContents .executeJavaScript( diff --git a/apps/browser/src/main/window-manager.ts b/apps/browser/src/main/window-manager.ts index 29ebc3f..f88be38 100644 --- a/apps/browser/src/main/window-manager.ts +++ b/apps/browser/src/main/window-manager.ts @@ -58,11 +58,6 @@ export class WindowManager { const fullscreenGapHorizontal = 57; // Match tab-manager const fullscreenGapVertical = 67; // Match tab-manager - const ts = new Date().toISOString().split("T")[1].slice(0, -1); - console.log(`[WindowManager][${ts}] updateWebContentsViewBounds called in FULLSCREEN mode`); - console.log(`[WindowManager][${ts}] Window bounds:`, windowBounds); - console.log(`[WindowManager][${ts}] Orientation: ${this.state.isLandscape ? 'LANDSCAPE' : 'PORTRAIT'}`); - if (this.state.isLandscape) { // Landscape: gap on left and right to avoid rounded corners const bounds = { @@ -71,7 +66,6 @@ export class WindowManager { width: windowBounds.width - fullscreenGapHorizontal * 2, height: windowBounds.height - topBarHeight - deviceFramePadding * 2, }; - console.log(`[WindowManager][${ts}] Setting LANDSCAPE bounds:`, bounds); activeTab.view.setBounds(bounds); } else { // Portrait: gap on top and bottom to avoid rounded corners @@ -81,7 +75,6 @@ export class WindowManager { width: windowBounds.width - deviceFramePadding * 2, height: windowBounds.height - topBarHeight - fullscreenGapVertical - fullscreenGapVertical, }; - console.log(`[WindowManager][${ts}] Setting PORTRAIT bounds:`, bounds); activeTab.view.setBounds(bounds); } diff --git a/apps/browser/src/webview-preload.ts b/apps/browser/src/webview-preload.ts index bacba7d..740ac0d 100644 --- a/apps/browser/src/webview-preload.ts +++ b/apps/browser/src/webview-preload.ts @@ -73,12 +73,6 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { const wasFullscreen = isFullscreenActive; isFullscreenActive = state; - const timestamp = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Preload][${timestamp}] Fullscreen state changed: ${state}`); - console.log(`[Preload][${timestamp}] Window size: ${window.innerWidth}x${window.innerHeight}`); - console.log(`[Preload][${timestamp}] Screen size (overridden): ${window.screen.width}x${window.screen.height}`); - console.log(`[Preload][${timestamp}] Original screen size: ${originalScreenWidth}x${originalScreenHeight}`); - if (state && !wasFullscreen) { // Entering fullscreen fullscreenElement = document.documentElement; // Assume whole document @@ -91,9 +85,6 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { const event = new Event("fullscreenchange", { bubbles: true }); document.dispatchEvent(event); - - const ts1 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Preload][${ts1}] ✅ Entered fullscreen mode`); } else if (!state && wasFullscreen) { // Exiting fullscreen fullscreenElement = null; @@ -106,9 +97,6 @@ ipcRenderer.on("set-fullscreen-state", (_event, state: boolean) => { const event = new Event("fullscreenchange", { bubbles: true }); document.dispatchEvent(event); - - const ts2 = new Date().toISOString().split('T')[1].slice(0, -1); - console.log(`[Preload][${ts2}] ✅ Exited fullscreen mode`); } });