From 97dda4dbe8d02508da84819c576b88aaea077c70 Mon Sep 17 00:00:00 2001 From: iliya Date: Sun, 22 Feb 2026 23:13:13 +0200 Subject: [PATCH] feat: implement fullscreen functionality and related IPC communication - Added WINDOW_FULLSCREEN_CHANGED and WINDOW_IS_FULLSCREEN constants for fullscreen state management. - Enhanced main window to notify the renderer when entering or leaving fullscreen mode. - Implemented IPC handlers to check fullscreen status and notify changes to the renderer. - Introduced useFullScreen hook in the renderer to manage fullscreen state and adjust UI accordingly. These changes improve user experience by allowing dynamic adjustments to the UI based on fullscreen status. --- src/main/index.ts | 19 +++++++++- src/main/ipc/window.ts | 7 ++++ src/preload/constants/ipcChannels.ts | 6 +++ src/preload/index.ts | 12 ++++++ src/renderer/api/httpClient.ts | 5 +++ .../components/layout/SidebarHeader.tsx | 2 +- .../components/layout/TabbedLayout.tsx | 9 ++++- src/renderer/hooks/useFullScreen.ts | 38 +++++++++++++++++++ src/shared/types/api.ts | 4 ++ 9 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 src/renderer/hooks/useFullScreen.ts diff --git a/src/main/index.ts b/src/main/index.ts index fa7ef79b..91b8653f 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -46,6 +46,7 @@ const logger = createLogger('App'); // IPC channel constants (duplicated from @preload to avoid boundary violation) const SSH_STATUS = 'ssh:status'; const CONTEXT_CHANGED = 'context:changed'; +const WINDOW_FULLSCREEN_CHANGED = 'window:fullscreen-changed'; const HTTP_SERVER_START = 'httpServer:start'; const HTTP_SERVER_STOP = 'httpServer:stop'; const HTTP_SERVER_GET_STATUS = 'httpServer:getStatus'; @@ -491,11 +492,27 @@ function createWindow(): void { }); } + // Notify renderer when entering/leaving fullscreen (so traffic light padding can be removed) + mainWindow.on('enter-full-screen', () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(WINDOW_FULLSCREEN_CHANGED, true); + } + }); + mainWindow.on('leave-full-screen', () => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(WINDOW_FULLSCREEN_CHANGED, false); + } + }); + // Set traffic light position + notify renderer on first load, and auto-check for updates mainWindow.webContents.on('did-finish-load', () => { if (mainWindow && !mainWindow.isDestroyed()) { syncTrafficLightPosition(mainWindow); - // Auto-check for updates 3 seconds after window loads + setTimeout(() => { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send(WINDOW_FULLSCREEN_CHANGED, mainWindow.isFullScreen()); + } + }, 0); setTimeout(() => updaterService.checkForUpdates(), 3000); } }); diff --git a/src/main/ipc/window.ts b/src/main/ipc/window.ts index d2cb1edc..05b9a5d6 100644 --- a/src/main/ipc/window.ts +++ b/src/main/ipc/window.ts @@ -4,6 +4,7 @@ * can provide conventional min / maximize / close buttons. */ +import { WINDOW_IS_FULLSCREEN } from '@preload/constants/ipcChannels'; import { createLogger } from '@shared/utils/logger'; import { BrowserWindow, type IpcMain } from 'electron'; @@ -40,6 +41,11 @@ export function registerWindowHandlers(ipcMain: IpcMain): void { return win != null && !win.isDestroyed() && win.isMaximized(); }); + ipcMain.handle(WINDOW_IS_FULLSCREEN, (): boolean => { + const win = getMainWindow(); + return win != null && !win.isDestroyed() && win.isFullScreen(); + }); + logger.info('Window handlers registered'); } @@ -48,5 +54,6 @@ export function removeWindowHandlers(ipcMain: IpcMain): void { ipcMain.removeHandler('window:maximize'); ipcMain.removeHandler('window:close'); ipcMain.removeHandler('window:isMaximized'); + ipcMain.removeHandler(WINDOW_IS_FULLSCREEN); logger.info('Window handlers removed'); } diff --git a/src/preload/constants/ipcChannels.ts b/src/preload/constants/ipcChannels.ts index 46092143..1d0b6fcc 100644 --- a/src/preload/constants/ipcChannels.ts +++ b/src/preload/constants/ipcChannels.ts @@ -172,6 +172,12 @@ export const WINDOW_CLOSE = 'window:close'; /** Whether the window is currently maximized */ export const WINDOW_IS_MAXIMIZED = 'window:isMaximized'; +/** Whether the window is in fullscreen (macOS native fullscreen) */ +export const WINDOW_IS_FULLSCREEN = 'window:isFullScreen'; + +/** Event: (isFullScreen: boolean) when window enters or leaves fullscreen */ +export const WINDOW_FULLSCREEN_CHANGED = 'window:fullscreen-changed'; + // ============================================================================= // Team API Channels // ============================================================================= diff --git a/src/preload/index.ts b/src/preload/index.ts index 259491f8..de79a6b3 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -48,6 +48,8 @@ import { UPDATER_INSTALL, UPDATER_STATUS, WINDOW_CLOSE, + WINDOW_FULLSCREEN_CHANGED, + WINDOW_IS_FULLSCREEN, WINDOW_IS_MAXIMIZED, WINDOW_MAXIMIZE, WINDOW_MINIMIZE, @@ -394,6 +396,16 @@ const electronAPI: ElectronAPI = { maximize: () => ipcRenderer.invoke(WINDOW_MAXIMIZE), close: () => ipcRenderer.invoke(WINDOW_CLOSE), isMaximized: () => ipcRenderer.invoke(WINDOW_IS_MAXIMIZED) as Promise, + isFullScreen: () => ipcRenderer.invoke(WINDOW_IS_FULLSCREEN) as Promise, + }, + + onFullScreenChange: (callback: (isFullScreen: boolean) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, isFullScreen: boolean): void => + callback(isFullScreen); + ipcRenderer.on(WINDOW_FULLSCREEN_CHANGED, listener); + return (): void => { + ipcRenderer.removeListener(WINDOW_FULLSCREEN_CHANGED, listener); + }; }, onTodoChange: (callback: (event: IpcFileChangePayload) => void): (() => void) => { diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index 6334a0a6..15655266 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -529,8 +529,13 @@ export class HttpAPIClient implements ElectronAPI { maximize: async (): Promise => {}, close: async (): Promise => {}, isMaximized: async (): Promise => false, + isFullScreen: async (): Promise => false, }; + onFullScreenChange = + (_callback: (isFullScreen: boolean) => void): (() => void) => + () => {}; + // --------------------------------------------------------------------------- // Updater (browser no-ops) // --------------------------------------------------------------------------- diff --git a/src/renderer/components/layout/SidebarHeader.tsx b/src/renderer/components/layout/SidebarHeader.tsx index f320bb1f..457c99ef 100644 --- a/src/renderer/components/layout/SidebarHeader.tsx +++ b/src/renderer/components/layout/SidebarHeader.tsx @@ -251,7 +251,7 @@ export const SidebarHeader = (): React.JSX.Element => { > {/* ROW 1: Logo in corner, project selector fills width, collapse button */}
{ - // Enable keyboard shortcuts useKeyboardShortcuts(); const zoomFactor = useZoomFactor(); - const trafficLightPadding = isElectronMode() ? getTrafficLightPaddingForZoom(zoomFactor) : 0; + const isFullScreen = useFullScreen(); + const trafficLightPadding = !isElectronMode() + ? 0 + : isFullScreen + ? 8 + : getTrafficLightPaddingForZoom(zoomFactor); return (
{ + if (!isElectronMode()) return; + + const { isFullScreen: isFullScreenFn } = api.windowControls; + if (typeof isFullScreenFn !== 'function') return; + + let cancelled = false; + + void isFullScreenFn().then((full) => { + if (!cancelled) setIsFullScreen(full); + }); + + const unsub = + typeof api.onFullScreenChange === 'function' + ? api.onFullScreenChange((full) => { + if (!cancelled) setIsFullScreen(full); + }) + : () => {}; + + return () => { + cancelled = true; + unsub(); + }; + }, []); + + return isFullScreen; +} diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index d82dfa9f..a3366140 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -466,8 +466,12 @@ export interface ElectronAPI { maximize: () => Promise; close: () => Promise; isMaximized: () => Promise; + isFullScreen: () => Promise; }; + /** Subscribe to fullscreen changes (e.g. to remove macOS traffic light padding in fullscreen) */ + onFullScreenChange: (callback: (isFullScreen: boolean) => void) => () => void; + // Updater API updater: UpdaterAPI;