From a33e6c6ae3ec94f5318f2ccf9dca8fea8aeea8dd Mon Sep 17 00:00:00 2001 From: Will Chen Date: Tue, 29 Apr 2025 11:41:40 -0700 Subject: [PATCH] Custom window controls (#46) --- src/app/TitleBar.tsx | 99 +++++++++++++++++++++++++++++ src/ipc/handlers/window_handlers.ts | 53 +++++++++++++++ src/ipc/ipc_client.ts | 41 ++++++++++++ src/ipc/ipc_host.ts | 2 + src/main.ts | 2 +- src/preload.ts | 4 ++ 6 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/ipc/handlers/window_handlers.ts diff --git a/src/app/TitleBar.tsx b/src/app/TitleBar.tsx index 68ae85a..b583686 100644 --- a/src/app/TitleBar.tsx +++ b/src/app/TitleBar.tsx @@ -11,6 +11,8 @@ import { cn } from "@/lib/utils"; import { useDeepLink } from "@/contexts/DeepLinkContext"; import { useEffect, useState } from "react"; import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog"; +import { useTheme } from "@/contexts/ThemeContext"; +import { IpcClient } from "@/ipc/ipc_client"; export const TitleBar = () => { const [selectedAppId] = useAtom(selectedAppIdAtom); @@ -18,6 +20,21 @@ export const TitleBar = () => { const { navigate } = useRouter(); const { settings, refreshSettings } = useSettings(); const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false); + const [showWindowControls, setShowWindowControls] = useState(false); + + useEffect(() => { + // Check if we're running on Windows + const checkPlatform = async () => { + try { + const platform = await IpcClient.getInstance().getSystemPlatform(); + setShowWindowControls(platform !== "darwin"); + } catch (error) { + console.error("Failed to get platform info:", error); + } + }; + + checkPlatform(); + }, []); const showDyadProSuccessDialog = () => { setIsSuccessDialogOpen(true); @@ -82,6 +99,7 @@ export const TitleBar = () => { {isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"} )} + {showWindowControls && } { ); }; + +function WindowsControls() { + const { isDarkMode } = useTheme(); + const ipcClient = IpcClient.getInstance(); + + const minimizeWindow = () => { + ipcClient.minimizeWindow(); + }; + + const maximizeWindow = () => { + ipcClient.maximizeWindow(); + }; + + const closeWindow = () => { + ipcClient.closeWindow(); + }; + + return ( +
+ + + +
+ ); +} diff --git a/src/ipc/handlers/window_handlers.ts b/src/ipc/handlers/window_handlers.ts new file mode 100644 index 0000000..eecf44c --- /dev/null +++ b/src/ipc/handlers/window_handlers.ts @@ -0,0 +1,53 @@ +import { BrowserWindow, ipcMain } from "electron"; +import log from "electron-log"; +import { platform } from "os"; + +const logger = log.scope("window-handlers"); + +// Handler for minimizing the window +const handleMinimize = (event: Electron.IpcMainInvokeEvent) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (!window) { + logger.error("Failed to get BrowserWindow instance for minimize command"); + return; + } + window.minimize(); +}; + +// Handler for maximizing/restoring the window +const handleMaximize = (event: Electron.IpcMainInvokeEvent) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (!window) { + logger.error("Failed to get BrowserWindow instance for maximize command"); + return; + } + + if (window.isMaximized()) { + window.restore(); + } else { + window.maximize(); + } +}; + +// Handler for closing the window +const handleClose = (event: Electron.IpcMainInvokeEvent) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (!window) { + logger.error("Failed to get BrowserWindow instance for close command"); + return; + } + window.close(); +}; + +// Handler to get the current system platform +const handleGetSystemPlatform = () => { + return platform(); +}; + +export function registerWindowHandlers() { + logger.debug("Registering window control handlers"); + ipcMain.handle("window:minimize", handleMinimize); + ipcMain.handle("window:maximize", handleMaximize); + ipcMain.handle("window:close", handleClose); + ipcMain.handle("window:get-platform", handleGetSystemPlatform); +} diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 37739e4..a8cff00 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -776,4 +776,45 @@ export class IpcClient { throw error; } } + + // Window control methods + public async minimizeWindow(): Promise { + try { + await this.ipcRenderer.invoke("window:minimize"); + } catch (error) { + showError(error); + throw error; + } + } + + public async maximizeWindow(): Promise { + try { + await this.ipcRenderer.invoke("window:maximize"); + } catch (error) { + showError(error); + throw error; + } + } + + public async closeWindow(): Promise { + try { + await this.ipcRenderer.invoke("window:close"); + } catch (error) { + showError(error); + throw error; + } + } + + // Get system platform (win32, darwin, linux) + public async getSystemPlatform(): Promise { + try { + const platform = await this.ipcRenderer.invoke("window:get-platform"); + return platform; + } catch (error) { + showError(error); + throw error; + } + } + + // --- End window control methods --- } diff --git a/src/ipc/ipc_host.ts b/src/ipc/ipc_host.ts index 2f443c4..6328f06 100644 --- a/src/ipc/ipc_host.ts +++ b/src/ipc/ipc_host.ts @@ -11,6 +11,7 @@ import { registerDebugHandlers } from "./handlers/debug_handlers"; import { registerSupabaseHandlers } from "./handlers/supabase_handlers"; import { registerLocalModelHandlers } from "./handlers/local_model_handlers"; import { registerTokenCountHandlers } from "./handlers/token_count_handlers"; +import { registerWindowHandlers } from "./handlers/window_handlers"; export function registerIpcHandlers() { // Register all IPC handlers by category @@ -27,4 +28,5 @@ export function registerIpcHandlers() { registerSupabaseHandlers(); registerLocalModelHandlers(); registerTokenCountHandlers(); + registerWindowHandlers(); } diff --git a/src/main.ts b/src/main.ts index d396b4e..a32aa14 100644 --- a/src/main.ts +++ b/src/main.ts @@ -96,7 +96,7 @@ const createWindow = () => { width: process.env.NODE_ENV === "development" ? 1280 : 960, height: 700, titleBarStyle: "hidden", - titleBarOverlay: true, + titleBarOverlay: false, trafficLightPosition: { x: 10, y: 8, diff --git a/src/preload.ts b/src/preload.ts index 9b22973..ad11fa3 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -48,6 +48,10 @@ const validInvokeChannels = [ "supabase:set-app-project", "supabase:unset-app-project", "local-models:list", + "window:minimize", + "window:maximize", + "window:close", + "window:get-platform", ] as const; // Add valid receive channels