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:
@@ -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.');
|
||||||
|
|||||||
@@ -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'),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<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()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/types/electron.d.ts
vendored
4
src/types/electron.d.ts
vendored
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user