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() {
|
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 (
|
return (
|
||||||
<div className="no-drag flex items-center gap-2">
|
<div
|
||||||
<button
|
className={cn(
|
||||||
onClick={() => window.electronWindow.close()}
|
"no-drag flex items-center gap-0.5"
|
||||||
className="group flex h-3 w-3 items-center justify-center rounded-full bg-[#FF5F57] transition-colors hover:bg-[#FF5F57]/80"
|
)}
|
||||||
aria-label="Close"
|
>
|
||||||
>
|
{buttons}
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
@@ -17,6 +17,8 @@ declare global {
|
|||||||
close: () => void;
|
close: () => void;
|
||||||
openHuddle: (data: { token: string; serverUrl: string }) => void;
|
openHuddle: (data: { token: string; serverUrl: string }) => void;
|
||||||
closeHuddle: () => void;
|
closeHuddle: () => void;
|
||||||
|
platform: NodeJS.Platform;
|
||||||
|
onMaximizeChange: (callback: (isMaximized: boolean) => void) => () => void;
|
||||||
};
|
};
|
||||||
electronHuddle: {
|
electronHuddle: {
|
||||||
getScreenSources: () => Promise<ScreenSource[]>;
|
getScreenSources: () => Promise<ScreenSource[]>;
|
||||||
|
|||||||
@@ -92,10 +92,10 @@ function TopBar() {
|
|||||||
const { data: particle } = useParticle(path);
|
const { data: particle } = useParticle(path);
|
||||||
|
|
||||||
return (
|
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 />
|
<WindowControls />
|
||||||
|
|
||||||
<Breadcrumb className="no-drag">
|
<Breadcrumb className="no-drag px-2">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem className="text-xs">
|
<BreadcrumbItem className="text-xs">
|
||||||
{segments.length === 0 ? (
|
{segments.length === 0 ? (
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export function TopBar({ networkId, particle, streamParticle }: TopBarProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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 />
|
<WindowControls />
|
||||||
|
|
||||||
<Breadcrumb className="no-drag rounded-full bg-black/30 backdrop-blur-sm px-3 py-1 mx-auto">
|
<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" />
|
<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 */}
|
{/* 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} />
|
<TopBar networkId={networkId} particle={currentParticle} streamParticle={streamParticle} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,13 @@ const createWindow = () => {
|
|||||||
});
|
});
|
||||||
hardenWindow(mainWindow);
|
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) {
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||||
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ contextBridge.exposeInMainWorld('electronWindow', {
|
|||||||
close: () => ipcRenderer.send('window:close'),
|
close: () => ipcRenderer.send('window:close'),
|
||||||
openHuddle: (data: { token: string; serverUrl: string }) => ipcRenderer.send('window:open-huddle', data),
|
openHuddle: (data: { token: string; serverUrl: string }) => ipcRenderer.send('window:open-huddle', data),
|
||||||
closeHuddle: () => ipcRenderer.send('window:close-huddle'),
|
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', {
|
contextBridge.exposeInMainWorld('electronHuddle', {
|
||||||
|
|||||||
Reference in New Issue
Block a user