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.
This commit is contained in:
iliya 2026-02-22 23:13:13 +02:00 committed by Илия
parent 2863320b91
commit 97dda4dbe8
9 changed files with 98 additions and 4 deletions

View file

@ -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);
}
});

View file

@ -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');
}

View file

@ -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
// =============================================================================

View file

@ -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<boolean>,
isFullScreen: () => ipcRenderer.invoke(WINDOW_IS_FULLSCREEN) as Promise<boolean>,
},
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) => {

View file

@ -529,8 +529,13 @@ export class HttpAPIClient implements ElectronAPI {
maximize: async (): Promise<void> => {},
close: async (): Promise<void> => {},
isMaximized: async (): Promise<boolean> => false,
isFullScreen: async (): Promise<boolean> => false,
};
onFullScreenChange =
(_callback: (isFullScreen: boolean) => void): (() => void) =>
() => {};
// ---------------------------------------------------------------------------
// Updater (browser no-ops)
// ---------------------------------------------------------------------------

View file

@ -251,7 +251,7 @@ export const SidebarHeader = (): React.JSX.Element => {
>
{/* ROW 1: Logo in corner, project selector fills width, collapse button */}
<div
className="flex select-none items-center gap-2 pr-2"
className="flex select-none items-center gap-1.5 pr-1"
style={
{
height: `${HEADER_ROW1_HEIGHT}px`,

View file

@ -8,6 +8,7 @@
import { isElectronMode } from '@renderer/api';
import { getTrafficLightPaddingForZoom } from '@renderer/constants/layout';
import { useFullScreen } from '@renderer/hooks/useFullScreen';
import { useKeyboardShortcuts } from '@renderer/hooks/useKeyboardShortcuts';
import { useZoomFactor } from '@renderer/hooks/useZoomFactor';
@ -21,10 +22,14 @@ import { Sidebar } from './Sidebar';
import { WindowsTitleBar } from './WindowsTitleBar';
export const TabbedLayout = (): React.JSX.Element => {
// 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 (
<div

View file

@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';
import { api, isElectronMode } from '@renderer/api';
/**
* Returns whether the window is in native fullscreen (macOS green button).
* When true, traffic light padding should be 0 so content can use the full width.
*/
export function useFullScreen(): boolean {
const [isFullScreen, setIsFullScreen] = useState(false);
useEffect(() => {
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;
}

View file

@ -466,8 +466,12 @@ export interface ElectronAPI {
maximize: () => Promise<void>;
close: () => Promise<void>;
isMaximized: () => Promise<boolean>;
isFullScreen: () => Promise<boolean>;
};
/** Subscribe to fullscreen changes (e.g. to remove macOS traffic light padding in fullscreen) */
onFullScreenChange: (callback: (isFullScreen: boolean) => void) => () => void;
// Updater API
updater: UpdaterAPI;