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:
@@ -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) => {
|
ipcMain.handle('window-minimize', async (event) => {
|
||||||
const win = BrowserWindow.fromWebContents(event.sender);
|
const win = BrowserWindow.fromWebContents(event.sender);
|
||||||
if (win) {
|
if (win) {
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ 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),
|
||||||
|
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'),
|
windowMinimize: () => ipcRenderer.invoke('window-minimize'),
|
||||||
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
windowMaximize: () => ipcRenderer.invoke('window-maximize'),
|
||||||
windowClose: () => ipcRenderer.invoke('window-close'),
|
windowClose: () => ipcRenderer.invoke('window-close'),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export { useDialogState, useErrorDialogState } from './useDialogState'
|
|||||||
export { useNoteBrowser, type UseNoteBrowserReturn } from './useNoteBrowser'
|
export { useNoteBrowser, type UseNoteBrowserReturn } from './useNoteBrowser'
|
||||||
export { useNoteContent } from './useNoteContent'
|
export { useNoteContent } from './useNoteContent'
|
||||||
export { useAutoLoadTabContent } from './useAutoLoadTabContent'
|
export { useAutoLoadTabContent } from './useAutoLoadTabContent'
|
||||||
|
export { usePopOutTab } from './usePopOutTab'
|
||||||
|
|
||||||
export { useFileSystemController } from './useFileSystemController'
|
export { useFileSystemController } from './useFileSystemController'
|
||||||
export { useFileTabs } from './useFileTabs'
|
export { useFileTabs } from './useFileTabs'
|
||||||
|
|||||||
58
src/hooks/domain/usePopOutTab.ts
Normal file
58
src/hooks/domain/usePopOutTab.ts
Normal 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])
|
||||||
|
}
|
||||||
@@ -119,9 +119,29 @@ export const NoteBrowser = () => {
|
|||||||
}, [selectFile])
|
}, [selectFile])
|
||||||
|
|
||||||
const handlePopOut = useCallback(async (file: FileItem) => {
|
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)}`
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isMarkdown && showTOC) {
|
if (!isMarkdown && showTOC) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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'
|
import { Minus, Square, X, Maximize2 } from 'lucide-react'
|
||||||
|
import { usePopOutTab } from '@/hooks/domain/usePopOutTab'
|
||||||
|
|
||||||
export const PopoutPage = () => {
|
export const PopoutPage = () => {
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
@@ -13,6 +14,8 @@ export const PopoutPage = () => {
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [isMaximized, setIsMaximized] = useState(false)
|
const [isMaximized, setIsMaximized] = useState(false)
|
||||||
|
|
||||||
|
usePopOutTab()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const path = searchParams.get('path')
|
const path = searchParams.get('path')
|
||||||
const name = searchParams.get('name')
|
const name = searchParams.get('name')
|
||||||
|
|||||||
2
src/types/electron.d.ts
vendored
2
src/types/electron.d.ts
vendored
@@ -54,6 +54,8 @@ 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 }>
|
||||||
|
transferTabData: (windowId: number, tabData: any) => Promise<{ success: boolean; error?: string }>
|
||||||
|
onTabDataReceived: (callback: (tabData: any) => void) => () => void
|
||||||
windowMinimize: () => Promise<{ success: boolean }>
|
windowMinimize: () => Promise<{ success: boolean }>
|
||||||
windowMaximize: () => Promise<{ success: boolean; isMaximized?: boolean }>
|
windowMaximize: () => Promise<{ success: boolean; isMaximized?: boolean }>
|
||||||
windowClose: () => Promise<{ success: boolean }>
|
windowClose: () => Promise<{ success: boolean }>
|
||||||
|
|||||||
Reference in New Issue
Block a user