- Pass content, unsavedContent, isEditing via URL query params - PopoutPage reads directly from URL params on mount - Eliminates IPC race condition entirely - Direct store update with loaded:true to prevent auto-reload
180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { useSearchParams } from 'react-router-dom'
|
|
import type { FileItemDTO } from '@shared/types/file'
|
|
import { matchModule } from '@/lib/module-registry'
|
|
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 { useTabStore } from '@/stores'
|
|
|
|
export const PopoutPage = () => {
|
|
const [searchParams] = useSearchParams()
|
|
const [file, setFile] = useState<FileItemDTO | null>(null)
|
|
const [content, setContent] = useState<string>('')
|
|
const [unsavedContent, setUnsavedContent] = useState<string>('')
|
|
const [isEditing, setIsEditing] = useState(false)
|
|
const [isMaximized, setIsMaximized] = useState(false)
|
|
const [ready, setReady] = useState(false)
|
|
|
|
const { selectFile } = useTabStore()
|
|
|
|
useEffect(() => {
|
|
const path = searchParams.get('path')
|
|
const name = searchParams.get('name')
|
|
const contentParam = searchParams.get('content')
|
|
const unsavedParam = searchParams.get('unsaved')
|
|
const editingParam = searchParams.get('editing')
|
|
|
|
if (!path || !name) {
|
|
return
|
|
}
|
|
|
|
const decodedPath = decodeURIComponent(path)
|
|
const decodedName = decodeURIComponent(name)
|
|
const decodedContent = contentParam ? decodeURIComponent(contentParam) : ''
|
|
const decodedUnsaved = unsavedParam ? decodeURIComponent(unsavedParam) : decodedContent
|
|
const decodedEditing = editingParam === 'true'
|
|
|
|
setFile({
|
|
name: decodedName,
|
|
path: decodedPath,
|
|
type: 'file',
|
|
size: 0,
|
|
modified: new Date().toISOString(),
|
|
})
|
|
setContent(decodedContent)
|
|
setUnsavedContent(decodedUnsaved)
|
|
setIsEditing(decodedEditing)
|
|
|
|
useTabStore.setState((state) => {
|
|
const newTabs = new Map(state.tabs)
|
|
newTabs.set(decodedPath, {
|
|
file: {
|
|
name: decodedName,
|
|
path: decodedPath,
|
|
type: 'file' as const,
|
|
size: 0,
|
|
modified: new Date().toISOString(),
|
|
},
|
|
content: decodedContent,
|
|
unsavedContent: decodedUnsaved,
|
|
isEditing: decodedEditing,
|
|
loading: false,
|
|
loaded: true,
|
|
})
|
|
return {
|
|
tabs: newTabs,
|
|
activeTabId: decodedPath,
|
|
}
|
|
})
|
|
|
|
selectFile({
|
|
name: decodedName,
|
|
path: decodedPath,
|
|
type: 'file',
|
|
size: 0,
|
|
modified: new Date().toISOString(),
|
|
})
|
|
|
|
setReady(true)
|
|
|
|
window.electronAPI?.windowIsMaximized().then((result) => {
|
|
if (result.success) {
|
|
setIsMaximized(result.isMaximized)
|
|
}
|
|
})
|
|
}, [searchParams, selectFile])
|
|
|
|
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 (!file || !ready) {
|
|
return (
|
|
<div className="flex items-center justify-center h-screen bg-white dark:bg-gray-900">
|
|
<div className="text-gray-500">Loading...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const renderContent = () => {
|
|
if (file.path.startsWith('file-transfer-panel')) {
|
|
const queryString = file.path.includes('?') ? file.path.split('?')[1] : ''
|
|
const urlParams = new URLSearchParams(queryString)
|
|
const serverHost = urlParams.get('host') || ''
|
|
const port = parseInt(urlParams.get('port') || '3000', 10)
|
|
const password = urlParams.get('password') || undefined
|
|
return (
|
|
<FileTransferPage
|
|
serverHost={serverHost}
|
|
port={port}
|
|
password={password}
|
|
onClose={() => window.close()}
|
|
/>
|
|
)
|
|
}
|
|
|
|
if (file.path.startsWith('remote-desktop://') || file.path.startsWith('remote-git://')) {
|
|
const urlParams = new URLSearchParams(file.path.split('?')[1])
|
|
const url = urlParams.get('url') || 'https://www.baidu.com'
|
|
const deviceName = urlParams.get('device') || ''
|
|
return <RemoteTabPage url={url} title={file.name} deviceName={deviceName} />
|
|
}
|
|
|
|
const module = matchModule(file)
|
|
if (module) {
|
|
const Component = module.component
|
|
return <Component />
|
|
}
|
|
|
|
return <MarkdownTabPage file={file} onTocUpdated={() => {}} />
|
|
}
|
|
|
|
return (
|
|
<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()}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|