Custom window controls (#46)

This commit is contained in:
Will Chen
2025-04-29 11:41:40 -07:00
committed by GitHub
parent 672bd790fa
commit a33e6c6ae3
6 changed files with 200 additions and 1 deletions

View File

@@ -11,6 +11,8 @@ import { cn } from "@/lib/utils";
import { useDeepLink } from "@/contexts/DeepLinkContext"; import { useDeepLink } from "@/contexts/DeepLinkContext";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog"; import { DyadProSuccessDialog } from "@/components/DyadProSuccessDialog";
import { useTheme } from "@/contexts/ThemeContext";
import { IpcClient } from "@/ipc/ipc_client";
export const TitleBar = () => { export const TitleBar = () => {
const [selectedAppId] = useAtom(selectedAppIdAtom); const [selectedAppId] = useAtom(selectedAppIdAtom);
@@ -18,6 +20,21 @@ export const TitleBar = () => {
const { navigate } = useRouter(); const { navigate } = useRouter();
const { settings, refreshSettings } = useSettings(); const { settings, refreshSettings } = useSettings();
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false); 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 = () => { const showDyadProSuccessDialog = () => {
setIsSuccessDialogOpen(true); setIsSuccessDialogOpen(true);
@@ -82,6 +99,7 @@ export const TitleBar = () => {
{isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"} {isDyadProEnabled ? "Dyad Pro" : "Dyad Pro (disabled)"}
</Button> </Button>
)} )}
{showWindowControls && <WindowsControls />}
</div> </div>
<DyadProSuccessDialog <DyadProSuccessDialog
@@ -91,3 +109,84 @@ export const TitleBar = () => {
</> </>
); );
}; };
function WindowsControls() {
const { isDarkMode } = useTheme();
const ipcClient = IpcClient.getInstance();
const minimizeWindow = () => {
ipcClient.minimizeWindow();
};
const maximizeWindow = () => {
ipcClient.maximizeWindow();
};
const closeWindow = () => {
ipcClient.closeWindow();
};
return (
<div className="ml-auto flex no-app-region-drag">
<button
className="w-12 h-11 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
onClick={minimizeWindow}
aria-label="Minimize"
>
<svg
width="12"
height="1"
viewBox="0 0 12 1"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
width="12"
height="1"
fill={isDarkMode ? "#ffffff" : "#000000"}
/>
</svg>
</button>
<button
className="w-12 h-11 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
onClick={maximizeWindow}
aria-label="Maximize"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="11"
height="11"
stroke={isDarkMode ? "#ffffff" : "#000000"}
/>
</svg>
</button>
<button
className="w-12 h-11 flex items-center justify-center hover:bg-red-500 transition-colors"
onClick={closeWindow}
aria-label="Close"
>
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 1L11 11M1 11L11 1"
stroke={isDarkMode ? "#ffffff" : "#000000"}
strokeWidth="1.5"
/>
</svg>
</button>
</div>
);
}

View File

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

View File

@@ -776,4 +776,45 @@ export class IpcClient {
throw error; throw error;
} }
} }
// Window control methods
public async minimizeWindow(): Promise<void> {
try {
await this.ipcRenderer.invoke("window:minimize");
} catch (error) {
showError(error);
throw error;
}
}
public async maximizeWindow(): Promise<void> {
try {
await this.ipcRenderer.invoke("window:maximize");
} catch (error) {
showError(error);
throw error;
}
}
public async closeWindow(): Promise<void> {
try {
await this.ipcRenderer.invoke("window:close");
} catch (error) {
showError(error);
throw error;
}
}
// Get system platform (win32, darwin, linux)
public async getSystemPlatform(): Promise<string> {
try {
const platform = await this.ipcRenderer.invoke("window:get-platform");
return platform;
} catch (error) {
showError(error);
throw error;
}
}
// --- End window control methods ---
} }

View File

@@ -11,6 +11,7 @@ import { registerDebugHandlers } from "./handlers/debug_handlers";
import { registerSupabaseHandlers } from "./handlers/supabase_handlers"; import { registerSupabaseHandlers } from "./handlers/supabase_handlers";
import { registerLocalModelHandlers } from "./handlers/local_model_handlers"; import { registerLocalModelHandlers } from "./handlers/local_model_handlers";
import { registerTokenCountHandlers } from "./handlers/token_count_handlers"; import { registerTokenCountHandlers } from "./handlers/token_count_handlers";
import { registerWindowHandlers } from "./handlers/window_handlers";
export function registerIpcHandlers() { export function registerIpcHandlers() {
// Register all IPC handlers by category // Register all IPC handlers by category
@@ -27,4 +28,5 @@ export function registerIpcHandlers() {
registerSupabaseHandlers(); registerSupabaseHandlers();
registerLocalModelHandlers(); registerLocalModelHandlers();
registerTokenCountHandlers(); registerTokenCountHandlers();
registerWindowHandlers();
} }

View File

@@ -96,7 +96,7 @@ const createWindow = () => {
width: process.env.NODE_ENV === "development" ? 1280 : 960, width: process.env.NODE_ENV === "development" ? 1280 : 960,
height: 700, height: 700,
titleBarStyle: "hidden", titleBarStyle: "hidden",
titleBarOverlay: true, titleBarOverlay: false,
trafficLightPosition: { trafficLightPosition: {
x: 10, x: 10,
y: 8, y: 8,

View File

@@ -48,6 +48,10 @@ const validInvokeChannels = [
"supabase:set-app-project", "supabase:set-app-project",
"supabase:unset-app-project", "supabase:unset-app-project",
"local-models:list", "local-models:list",
"window:minimize",
"window:maximize",
"window:close",
"window:get-platform",
] as const; ] as const;
// Add valid receive channels // Add valid receive channels