From aa5895873b399a468c5dde1a455fa318db7fa848 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Sat, 21 Mar 2026 23:52:02 +0800 Subject: [PATCH] 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 --- electron/main.ts | 14 ++++++++ electron/preload.ts | 6 ++++ src/hooks/domain/index.ts | 1 + src/hooks/domain/usePopOutTab.ts | 58 ++++++++++++++++++++++++++++++++ src/pages/NoteBrowser.tsx | 24 +++++++++++-- src/pages/PopoutPage.tsx | 3 ++ src/types/electron.d.ts | 2 ++ 7 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/hooks/domain/usePopOutTab.ts diff --git a/electron/main.ts b/electron/main.ts index 4a36b14..2ce7263 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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) { diff --git a/electron/preload.ts b/electron/preload.ts index 57f0541..2b518e8 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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'), diff --git a/src/hooks/domain/index.ts b/src/hooks/domain/index.ts index 63b4ee3..3bf1271 100644 --- a/src/hooks/domain/index.ts +++ b/src/hooks/domain/index.ts @@ -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' diff --git a/src/hooks/domain/usePopOutTab.ts b/src/hooks/domain/usePopOutTab.ts new file mode 100644 index 0000000..aa9fe4d --- /dev/null +++ b/src/hooks/domain/usePopOutTab.ts @@ -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]) +} diff --git a/src/pages/NoteBrowser.tsx b/src/pages/NoteBrowser.tsx index e5392ca..eb32a68 100644 --- a/src/pages/NoteBrowser.tsx +++ b/src/pages/NoteBrowser.tsx @@ -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) { diff --git a/src/pages/PopoutPage.tsx b/src/pages/PopoutPage.tsx index 36bfd97..62beca4 100644 --- a/src/pages/PopoutPage.tsx +++ b/src/pages/PopoutPage.tsx @@ -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(null) const [isMaximized, setIsMaximized] = useState(false) + usePopOutTab() + useEffect(() => { const path = searchParams.get('path') const name = searchParams.get('name') diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 49c3ade..179ee1c 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -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 }>