feat: transfer tab state to popout window and close tab in main window

- Restore transfer-tab-data IPC for transferring tab state
- Create usePopOutTab hook to receive tab data in new window
- Update handlePopOut to transfer data and close tab in main window
- Add PopOutTabData interface for type safety
This commit is contained in:
2026-03-21 23:52:02 +08:00
parent c37e6ab4f2
commit aa5895873b
7 changed files with 106 additions and 2 deletions

View File

@@ -491,6 +491,20 @@ ipcMain.handle('create-window', async (_event, tabData: { route: string; title:
}
});
ipcMain.handle('transfer-tab-data', async (_event, windowId: number, tabData: any) => {
try {
const win = electronState.getWindow(windowId);
if (!win) {
return { success: false, error: 'Window not found' };
}
win.webContents.send('tab-data-received', tabData);
return { success: true };
} catch (error: any) {
log.error('[PopOut] Failed to transfer tab data:', error);
return { success: false, error: error.message };
}
});
ipcMain.handle('window-minimize', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {

View File

@@ -54,6 +54,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
terminalGetStatus: () => ipcRenderer.invoke('terminal-get-status'),
terminalGetPort: () => ipcRenderer.invoke('terminal-get-port'),
createWindow: (tabData: { route: string; title: string }) => ipcRenderer.invoke('create-window', tabData),
transferTabData: (windowId: number, tabData: any) => ipcRenderer.invoke('transfer-tab-data', windowId, tabData),
onTabDataReceived: (callback: (tabData: any) => void) => {
const handler = (_event: Electron.IpcRendererEvent, tabData: any) => callback(tabData);
ipcRenderer.on('tab-data-received', handler);
return () => ipcRenderer.removeListener('tab-data-received', handler);
},
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
windowClose: () => ipcRenderer.invoke('window-close'),

View File

@@ -3,6 +3,7 @@ export { useDialogState, useErrorDialogState } from './useDialogState'
export { useNoteBrowser, type UseNoteBrowserReturn } from './useNoteBrowser'
export { useNoteContent } from './useNoteContent'
export { useAutoLoadTabContent } from './useAutoLoadTabContent'
export { usePopOutTab } from './usePopOutTab'
export { useFileSystemController } from './useFileSystemController'
export { useFileTabs } from './useFileTabs'

View File

@@ -0,0 +1,58 @@
import { useEffect } from 'react'
import { useTabStore, TabState } from '@/stores'
export interface PopOutTabData {
file: {
name: string
path: string
type: 'file' | 'dir'
size: number
modified: string
}
content: string
unsavedContent: string
isEditing: boolean
loading: boolean
loaded: boolean
}
export function usePopOutTab() {
const { selectFile } = useTabStore()
useEffect(() => {
const unsubscribe = window.electronAPI?.onTabDataReceived((tabData: PopOutTabData) => {
console.log('[PopOut] Received tab data:', tabData)
if (!tabData?.file?.path) {
console.error('[PopOut] Invalid tab data received')
return
}
const { file, content, unsavedContent, isEditing, loading, loaded } = tabData
const newTab: TabState = {
file: file as any,
content: content || '',
unsavedContent: unsavedContent || content || '',
isEditing: isEditing || false,
loading: loading || false,
loaded: loaded !== undefined ? loaded : true,
}
useTabStore.setState((state) => {
const newTabs = new Map(state.tabs)
newTabs.set(file.path, newTab)
return {
tabs: newTabs,
activeTabId: file.path,
}
})
selectFile(file as any)
})
return () => {
unsubscribe?.()
}
}, [selectFile])
}

View File

@@ -119,9 +119,29 @@ export const NoteBrowser = () => {
}, [selectFile])
const handlePopOut = useCallback(async (file: FileItem) => {
const tabState = tabs.get(file.path)
if (!tabState) return
const tabData = {
file: file,
content: tabState.content,
unsavedContent: tabState.unsavedContent,
isEditing: tabState.isEditing,
loading: tabState.loading,
loaded: tabState.loaded,
}
const route = `/popout?path=${encodeURIComponent(file.path)}&name=${encodeURIComponent(file.name)}`
await window.electronAPI?.createWindow({ route, title: file.name })
}, [])
const result = await window.electronAPI?.createWindow({ route, title: file.name })
if (result?.success && result.windowId) {
setTimeout(() => {
window.electronAPI?.transferTabData(result.windowId!, tabData)
}, 500)
}
closeFile(file)
}, [tabs, closeFile])
useEffect(() => {
if (!isMarkdown && showTOC) {

View File

@@ -6,6 +6,7 @@ import { MarkdownTabPage } from '@/components/tabs/MarkdownTabPage'
import { RemoteTabPage } from '@/modules/remote/RemoteTabPage'
import { FileTransferPage } from '@/modules/remote/components/file-transfer/FileTransferPage'
import { Minus, Square, X, Maximize2 } from 'lucide-react'
import { usePopOutTab } from '@/hooks/domain/usePopOutTab'
export const PopoutPage = () => {
const [searchParams] = useSearchParams()
@@ -13,6 +14,8 @@ export const PopoutPage = () => {
const [error, setError] = useState<string | null>(null)
const [isMaximized, setIsMaximized] = useState(false)
usePopOutTab()
useEffect(() => {
const path = searchParams.get('path')
const name = searchParams.get('name')

View File

@@ -54,6 +54,8 @@ export interface ElectronAPI {
terminalGetStatus: () => Promise<{ running: boolean; port: number }>
terminalGetPort: () => Promise<{ port: number }>
createWindow: (tabData: { route: string; title: string }) => Promise<{ success: boolean; windowId?: number; error?: string }>
transferTabData: (windowId: number, tabData: any) => Promise<{ success: boolean; error?: string }>
onTabDataReceived: (callback: (tabData: any) => void) => () => void
windowMinimize: () => Promise<{ success: boolean }>
windowMaximize: () => Promise<{ success: boolean; isMaximized?: boolean }>
windowClose: () => Promise<{ success: boolean }>