use universal window controls across platforms
This commit is contained in:
@@ -1,33 +1,64 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Copy, Minus, Square, X } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function WindowControls() {
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
return window.electronWindow.onMaximizeChange(setIsMaximized);
|
||||
}, []);
|
||||
|
||||
const minimize = (
|
||||
<Button
|
||||
key="minimize"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => window.electronWindow.minimize()}
|
||||
aria-label="Minimize"
|
||||
className="dark:hover:bg-white/10 rounded text-white/50"
|
||||
>
|
||||
<Minus />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const maximize = (
|
||||
<Button
|
||||
key="maximize"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => window.electronWindow.maximize()}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
className="dark:hover:bg-white/10 rounded text-white/50"
|
||||
>
|
||||
{isMaximized ? <Copy /> : <Square />}
|
||||
</Button>
|
||||
);
|
||||
|
||||
const close = (
|
||||
<Button
|
||||
key="close"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => window.electronWindow.close()}
|
||||
aria-label="Close"
|
||||
className="hover:text-destructive dark:hover:bg-white/10 rounded text-white/50"
|
||||
>
|
||||
<X />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const buttons = [close, maximize, minimize];
|
||||
|
||||
return (
|
||||
<div className="no-drag flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => window.electronWindow.close()}
|
||||
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#FF5F57] transition-colors hover:bg-[#FF5F57]/80"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="h-1.5 w-1.5 opacity-0 group-hover:opacity-100" viewBox="0 0 6 6" fill="none">
|
||||
<path d="M1 1L5 5M5 1L1 5" stroke="rgba(0,0,0,0.5)" strokeWidth="1.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.electronWindow.minimize()}
|
||||
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#FEBC2E] transition-colors hover:bg-[#FEBC2E]/80"
|
||||
aria-label="Minimize"
|
||||
>
|
||||
<svg className="h-1.5 w-1.5 opacity-0 group-hover:opacity-100" viewBox="0 0 6 6" fill="none">
|
||||
<path d="M1 3H5" stroke="rgba(0,0,0,0.5)" strokeWidth="1.2" strokeLinecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => window.electronWindow.fullscreen()}
|
||||
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#28C840] transition-colors hover:bg-[#28C840]/80"
|
||||
aria-label="Maximize"
|
||||
>
|
||||
<svg className="h-1.5 w-1.5 opacity-0 group-hover:opacity-100" viewBox="0 0 6 6" fill="none">
|
||||
<path d="M1 1.5L3 0.5L5 1.5V4.5L3 5.5L1 4.5V1.5Z" stroke="rgba(0,0,0,0.5)" strokeWidth="0.8" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"no-drag flex items-center gap-0.5"
|
||||
)}
|
||||
>
|
||||
{buttons}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -17,6 +17,8 @@ declare global {
|
||||
close: () => void;
|
||||
openHuddle: (data: { token: string; serverUrl: string }) => void;
|
||||
closeHuddle: () => void;
|
||||
platform: NodeJS.Platform;
|
||||
onMaximizeChange: (callback: (isMaximized: boolean) => void) => () => void;
|
||||
};
|
||||
electronHuddle: {
|
||||
getScreenSources: () => Promise<ScreenSource[]>;
|
||||
|
||||
@@ -92,10 +92,10 @@ function TopBar() {
|
||||
const { data: particle } = useParticle(path);
|
||||
|
||||
return (
|
||||
<div className="drag-region flex items-center gap-3 border-b px-3 py-1 bg-muted">
|
||||
<div className="drag-region flex items-center gap-1 border-b bg-muted p-1">
|
||||
<WindowControls />
|
||||
|
||||
<Breadcrumb className="no-drag">
|
||||
<Breadcrumb className="no-drag px-2">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="text-xs">
|
||||
{segments.length === 0 ? (
|
||||
|
||||
@@ -77,7 +77,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="drag-region flex flex-row px-4 gap-1 items-center">
|
||||
<div className="drag-region flex flex-row px-2 gap-1 items-center">
|
||||
<WindowControls />
|
||||
|
||||
<Breadcrumb className="no-drag rounded-full bg-black/30 backdrop-blur-sm px-3 py-1 mx-auto">
|
||||
|
||||
@@ -419,7 +419,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
<div className="pointer-events-none absolute inset-x-0 top-0 z-[5] h-32 bg-gradient-to-b from-black/60 to-transparent" />
|
||||
|
||||
{/* TopBar — always visible */}
|
||||
<div className="z-10 absolute left-0 right-0 mt-3">
|
||||
<div className="z-10 absolute left-0 right-0 pt-2">
|
||||
<TopBar networkId={networkId} particle={currentParticle} streamParticle={streamParticle} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -124,6 +124,13 @@ const createWindow = () => {
|
||||
});
|
||||
hardenWindow(mainWindow);
|
||||
|
||||
mainWindow.on('maximize', () => {
|
||||
mainWindow?.webContents.send('window:maximize-changed', true);
|
||||
});
|
||||
mainWindow.on('unmaximize', () => {
|
||||
mainWindow?.webContents.send('window:maximize-changed', false);
|
||||
});
|
||||
|
||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
@@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld('electronWindow', {
|
||||
close: () => ipcRenderer.send('window:close'),
|
||||
openHuddle: (data: { token: string; serverUrl: string }) => ipcRenderer.send('window:open-huddle', data),
|
||||
closeHuddle: () => ipcRenderer.send('window:close-huddle'),
|
||||
platform: process.platform,
|
||||
onMaximizeChange: (callback: (isMaximized: boolean) => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, value: boolean) => callback(value);
|
||||
ipcRenderer.on('window:maximize-changed', handler);
|
||||
return () => { ipcRenderer.removeListener('window:maximize-changed', handler); };
|
||||
},
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('electronHuddle', {
|
||||
|
||||
Reference in New Issue
Block a user