feat: add titlebar to popout window with minimize/maximize/close buttons

- Add window-minimize, window-maximize, window-close, window-is-maximized IPC handlers
- Add titlebar with window controls to PopoutPage
- Update preload and types for new window control APIs
This commit is contained in:
2026-03-21 23:46:21 +08:00
parent f160adbdb1
commit c37e6ab4f2
4 changed files with 101 additions and 2 deletions

View File

@@ -491,6 +491,45 @@ ipcMain.handle('create-window', async (_event, tabData: { route: string; title:
} }
}); });
ipcMain.handle('window-minimize', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.minimize();
return { success: true };
}
return { success: false };
});
ipcMain.handle('window-maximize', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
return { success: true, isMaximized: win.isMaximized() };
}
return { success: false };
});
ipcMain.handle('window-close', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.close();
return { success: true };
}
return { success: false };
});
ipcMain.handle('window-is-maximized', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
return { success: true, isMaximized: win.isMaximized() };
}
return { success: false, isMaximized: false };
});
async function startServer() { async function startServer() {
if (electronState.isDevelopment()) { if (electronState.isDevelopment()) {
log.info('In dev mode, assuming external servers are running.'); log.info('In dev mode, assuming external servers are running.');

View File

@@ -54,4 +54,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
terminalGetStatus: () => ipcRenderer.invoke('terminal-get-status'), terminalGetStatus: () => ipcRenderer.invoke('terminal-get-status'),
terminalGetPort: () => ipcRenderer.invoke('terminal-get-port'), terminalGetPort: () => ipcRenderer.invoke('terminal-get-port'),
createWindow: (tabData: { route: string; title: string }) => ipcRenderer.invoke('create-window', tabData), createWindow: (tabData: { route: string; title: string }) => ipcRenderer.invoke('create-window', tabData),
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
windowClose: () => ipcRenderer.invoke('window-close'),
windowIsMaximized: () => ipcRenderer.invoke('window-is-maximized'),
}) })

View File

@@ -5,11 +5,13 @@ import { matchModule } from '@/lib/module-registry'
import { MarkdownTabPage } from '@/components/tabs/MarkdownTabPage' import { MarkdownTabPage } from '@/components/tabs/MarkdownTabPage'
import { RemoteTabPage } from '@/modules/remote/RemoteTabPage' import { RemoteTabPage } from '@/modules/remote/RemoteTabPage'
import { FileTransferPage } from '@/modules/remote/components/file-transfer/FileTransferPage' import { FileTransferPage } from '@/modules/remote/components/file-transfer/FileTransferPage'
import { Minus, Square, X, Maximize2 } from 'lucide-react'
export const PopoutPage = () => { export const PopoutPage = () => {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const [file, setFile] = useState<FileItemDTO | null>(null) const [file, setFile] = useState<FileItemDTO | null>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [isMaximized, setIsMaximized] = useState(false)
useEffect(() => { useEffect(() => {
const path = searchParams.get('path') const path = searchParams.get('path')
@@ -27,8 +29,29 @@ export const PopoutPage = () => {
size: 0, size: 0,
modified: new Date().toISOString(), modified: new Date().toISOString(),
}) })
window.electronAPI?.windowIsMaximized().then((result) => {
if (result.success) {
setIsMaximized(result.isMaximized)
}
})
}, [searchParams]) }, [searchParams])
const handleMinimize = () => {
window.electronAPI?.windowMinimize()
}
const handleMaximize = async () => {
const result = await window.electronAPI?.windowMaximize()
if (result?.success && result.isMaximized !== undefined) {
setIsMaximized(result.isMaximized)
}
}
const handleClose = () => {
window.electronAPI?.windowClose()
}
if (error) { if (error) {
return ( return (
<div className="flex items-center justify-center h-screen bg-white dark:bg-gray-900"> <div className="flex items-center justify-center h-screen bg-white dark:bg-gray-900">
@@ -79,8 +102,37 @@ export const PopoutPage = () => {
} }
return ( return (
<div className="h-screen w-screen overflow-hidden bg-white dark:bg-gray-900"> <div className="h-screen w-screen flex flex-col bg-white dark:bg-gray-900">
{renderContent()} <div className="titlebar-drag-region h-8 flex items-center justify-between px-3 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shrink-0">
<span className="text-sm text-gray-700 dark:text-gray-200 truncate">{file.name}</span>
<div className="flex items-center gap-1 titlebar-no-drag">
<button
onClick={handleMinimize}
className="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
>
<Minus size={14} className="text-gray-600 dark:text-gray-300" />
</button>
<button
onClick={handleMaximize}
className="p-1.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
>
{isMaximized ? (
<Square size={12} className="text-gray-600 dark:text-gray-300" />
) : (
<Maximize2 size={12} className="text-gray-600 dark:text-gray-300" />
)}
</button>
<button
onClick={handleClose}
className="p-1.5 hover:bg-red-500 hover:text-white rounded transition-colors"
>
<X size={14} className="text-gray-600 dark:text-gray-300" />
</button>
</div>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
{renderContent()}
</div>
</div> </div>
) )
} }

View File

@@ -54,6 +54,10 @@ export interface ElectronAPI {
terminalGetStatus: () => Promise<{ running: boolean; port: number }> terminalGetStatus: () => Promise<{ running: boolean; port: number }>
terminalGetPort: () => Promise<{ port: number }> terminalGetPort: () => Promise<{ port: number }>
createWindow: (tabData: { route: string; title: string }) => Promise<{ success: boolean; windowId?: number; error?: string }> createWindow: (tabData: { route: string; title: string }) => Promise<{ success: boolean; windowId?: number; error?: string }>
windowMinimize: () => Promise<{ success: boolean }>
windowMaximize: () => Promise<{ success: boolean; isMaximized?: boolean }>
windowClose: () => Promise<{ success: boolean }>
windowIsMaximized: () => Promise<{ success: boolean; isMaximized: boolean }>
} }
declare global { declare global {