import { useState, useEffect, useRef } from 'react'; import { Folder, File, FileCode, FileJson, FileImage, FileText, FolderOpen, Copy, Trash2, Edit3, LayoutGrid, List, ChevronsUp, RefreshCw } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub, FileActionRegistry } from '@esengine/editor-core'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { FileTree, FileTreeHandle } from './FileTree'; import { ResizablePanel } from './ResizablePanel'; import { ContextMenu, ContextMenuItem } from './ContextMenu'; import '../styles/AssetBrowser.css'; interface AssetItem { name: string; path: string; type: 'file' | 'folder'; extension?: string; size?: number; modified?: number; } interface AssetBrowserProps { projectPath: string | null; locale: string; onOpenScene?: (scenePath: string) => void; } export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserProps) { const messageHub = Core.services.resolve(MessageHub); const fileActionRegistry = Core.services.resolve(FileActionRegistry); const detailViewFileTreeRef = useRef(null); const treeOnlyViewFileTreeRef = useRef(null); const [currentPath, setCurrentPath] = useState(null); const [selectedPath, setSelectedPath] = useState(null); const [assets, setAssets] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); const [loading, setLoading] = useState(false); const [showDetailView, setShowDetailView] = useState(() => { const saved = localStorage.getItem('asset-browser-detail-view'); return saved !== null ? saved === 'true' : false; }); const [contextMenu, setContextMenu] = useState<{ position: { x: number; y: number }; asset: AssetItem; } | null>(null); const [renameDialog, setRenameDialog] = useState<{ asset: AssetItem; newName: string; } | null>(null); const [deleteConfirmDialog, setDeleteConfirmDialog] = useState(null); const translations = { en: { title: 'Content Browser', noProject: 'No project loaded', loading: 'Loading...', empty: 'No assets found', search: 'Search...', name: 'Name', type: 'Type', file: 'File', folder: 'Folder' }, zh: { title: '内容浏览器', noProject: '没有加载项目', loading: '加载中...', empty: '没有找到资产', search: '搜索...', name: '名称', type: '类型', file: '文件', folder: '文件夹' } }; const t = translations[locale as keyof typeof translations] || translations.en; useEffect(() => { if (projectPath) { setCurrentPath(projectPath); loadAssets(projectPath); } else { setAssets([]); setCurrentPath(null); setSelectedPath(null); } }, [projectPath]); // Listen for asset reveal requests useEffect(() => { const messageHub = Core.services.resolve(MessageHub); if (!messageHub) return; const unsubscribe = messageHub.subscribe('asset:reveal', (data: any) => { const filePath = data.path; if (filePath) { setSelectedPath(filePath); const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null; if (dirPath) { setCurrentPath(dirPath); loadAssets(dirPath); } } }); return () => unsubscribe(); }, []); const loadAssets = async (path: string) => { setLoading(true); try { const entries = await TauriAPI.listDirectory(path); const assetItems: AssetItem[] = entries.map((entry: DirectoryEntry) => { const extension = entry.is_dir ? undefined : (entry.name.includes('.') ? entry.name.split('.').pop() : undefined); return { name: entry.name, path: entry.path, type: entry.is_dir ? 'folder' as const : 'file' as const, extension, size: entry.size, modified: entry.modified }; }); setAssets(assetItems.sort((a, b) => { if (a.type === b.type) return a.name.localeCompare(b.name); return a.type === 'folder' ? -1 : 1; })); } catch (error) { console.error('Failed to load assets:', error); setAssets([]); } finally { setLoading(false); } }; const searchProjectRecursively = async (rootPath: string, query: string): Promise => { const results: AssetItem[] = []; const lowerQuery = query.toLowerCase(); const searchDirectory = async (dirPath: string) => { try { const entries = await TauriAPI.listDirectory(dirPath); for (const entry of entries) { if (entry.name.startsWith('.')) continue; if (entry.name.toLowerCase().includes(lowerQuery)) { const extension = entry.is_dir ? undefined : (entry.name.includes('.') ? entry.name.split('.').pop() : undefined); results.push({ name: entry.name, path: entry.path, type: entry.is_dir ? 'folder' as const : 'file' as const, extension, size: entry.size, modified: entry.modified }); } if (entry.is_dir) { await searchDirectory(entry.path); } } } catch (error) { console.error(`Failed to search directory ${dirPath}:`, error); } }; await searchDirectory(rootPath); return results.sort((a, b) => { if (a.type === b.type) return a.name.localeCompare(b.name); return a.type === 'folder' ? -1 : 1; }); }; useEffect(() => { const performSearch = async () => { if (!searchQuery.trim()) { setSearchResults([]); setIsSearching(false); return; } if (!projectPath) return; setIsSearching(true); try { const results = await searchProjectRecursively(projectPath, searchQuery); setSearchResults(results); } catch (error) { console.error('Search failed:', error); setSearchResults([]); } finally { setIsSearching(false); } }; const timeoutId = setTimeout(performSearch, 300); return () => clearTimeout(timeoutId); }, [searchQuery, projectPath]); const handleFolderSelect = (path: string) => { setCurrentPath(path); loadAssets(path); }; const handleAssetClick = (asset: AssetItem) => { setSelectedPath(asset.path); messageHub?.publish('asset-file:selected', { fileInfo: { name: asset.name, path: asset.path, extension: asset.extension, size: asset.size, modified: asset.modified, isDirectory: asset.type === 'folder' } }); }; const handleAssetDoubleClick = async (asset: AssetItem) => { if (asset.type === 'folder') { setCurrentPath(asset.path); loadAssets(asset.path); } else if (asset.type === 'file') { if (asset.extension === 'ecs' && onOpenScene) { onOpenScene(asset.path); return; } if (fileActionRegistry) { console.log('[AssetBrowser] Handling double click for:', asset.path); console.log('[AssetBrowser] Extension:', asset.extension); const handled = await fileActionRegistry.handleDoubleClick(asset.path); console.log('[AssetBrowser] Handled by plugin:', handled); if (handled) { return; } } else { console.log('[AssetBrowser] FileActionRegistry not available'); } try { await TauriAPI.openFileWithSystemApp(asset.path); } catch (error) { console.error('Failed to open file:', error); } } }; const handleRename = async (asset: AssetItem, newName: string) => { if (!newName.trim() || newName === asset.name) { setRenameDialog(null); return; } try { const lastSlash = Math.max(asset.path.lastIndexOf('/'), asset.path.lastIndexOf('\\')); const parentPath = asset.path.substring(0, lastSlash); const newPath = `${parentPath}/${newName}`; await TauriAPI.renameFileOrFolder(asset.path, newPath); // 刷新当前目录 if (currentPath) { await loadAssets(currentPath); } // 更新选中路径 if (selectedPath === asset.path) { setSelectedPath(newPath); } setRenameDialog(null); } catch (error) { console.error('Failed to rename:', error); alert(`重命名失败: ${error}`); } }; const handleDelete = async (asset: AssetItem) => { try { if (asset.type === 'folder') { await TauriAPI.deleteFolder(asset.path); } else { await TauriAPI.deleteFile(asset.path); } // 刷新当前目录 if (currentPath) { await loadAssets(currentPath); } // 清除选中状态 if (selectedPath === asset.path) { setSelectedPath(null); } setDeleteConfirmDialog(null); } catch (error) { console.error('Failed to delete:', error); alert(`删除失败: ${error}`); } }; const handleContextMenu = (e: React.MouseEvent, asset: AssetItem) => { e.preventDefault(); setContextMenu({ position: { x: e.clientX, y: e.clientY }, asset }); }; const getContextMenuItems = (asset: AssetItem): ContextMenuItem[] => { const items: ContextMenuItem[] = []; if (asset.type === 'file') { items.push({ label: locale === 'zh' ? '打开' : 'Open', icon: , onClick: () => handleAssetDoubleClick(asset) }); if (fileActionRegistry) { const handlers = fileActionRegistry.getHandlersForFile(asset.path); for (const handler of handlers) { if (handler.getContextMenuItems) { const parentPath = asset.path.substring(0, asset.path.lastIndexOf('/')); const pluginItems = handler.getContextMenuItems(asset.path, parentPath); for (const pluginItem of pluginItems) { items.push({ label: pluginItem.label, icon: pluginItem.icon, onClick: () => pluginItem.onClick(asset.path, parentPath), disabled: pluginItem.disabled, separator: pluginItem.separator }); } } } } items.push({ label: '', separator: true, onClick: () => {} }); } if (asset.type === 'folder' && fileActionRegistry) { const templates = fileActionRegistry.getCreationTemplates(); if (templates.length > 0) { items.push({ label: '', separator: true, onClick: () => {} }); for (const template of templates) { items.push({ label: `${locale === 'zh' ? '新建' : 'New'} ${template.label}`, icon: template.icon, onClick: async () => { const fileName = `${template.defaultFileName}.${template.extension}`; const filePath = `${asset.path}/${fileName}`; const content = await template.createContent(fileName); await TauriAPI.writeFileContent(filePath, content); if (currentPath) { await loadAssets(currentPath); } } }); } } } // 在文件管理器中显示 items.push({ label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer', icon: , onClick: async () => { try { await TauriAPI.showInFolder(asset.path); } catch (error) { console.error('Failed to show in folder:', error); } } }); items.push({ label: '', separator: true, onClick: () => {} }); // 复制路径 items.push({ label: locale === 'zh' ? '复制路径' : 'Copy Path', icon: , onClick: () => { navigator.clipboard.writeText(asset.path); } }); items.push({ label: '', separator: true, onClick: () => {} }); // 重命名 items.push({ label: locale === 'zh' ? '重命名' : 'Rename', icon: , onClick: () => { setRenameDialog({ asset, newName: asset.name }); setContextMenu(null); }, disabled: false }); // 删除 items.push({ label: locale === 'zh' ? '删除' : 'Delete', icon: , onClick: () => { setDeleteConfirmDialog(asset); setContextMenu(null); }, disabled: false }); return items; }; const getBreadcrumbs = () => { if (!currentPath || !projectPath) return []; const relative = currentPath.replace(projectPath, ''); const parts = relative.split(/[/\\]/).filter((p) => p); const crumbs = [{ name: 'Content', path: projectPath }]; let accPath = projectPath; for (const part of parts) { accPath = `${accPath}${accPath.endsWith('\\') || accPath.endsWith('/') ? '' : '/'}${part}`; crumbs.push({ name: part, path: accPath }); } return crumbs; }; const filteredAssets = searchQuery.trim() ? searchResults : assets; const getRelativePath = (fullPath: string): string => { if (!projectPath) return fullPath; const relativePath = fullPath.replace(projectPath, '').replace(/^[/\\]/, ''); const parts = relativePath.split(/[/\\]/); return parts.slice(0, -1).join('/'); }; const getFileIcon = (asset: AssetItem) => { if (asset.type === 'folder') { // 检查是否为框架专用文件夹 const folderName = asset.name.toLowerCase(); if (folderName === 'plugins' || folderName === '.ecs') { return ; } return ; } const ext = asset.extension?.toLowerCase(); switch (ext) { case 'ecs': return ; case 'btree': return ; case 'ts': case 'tsx': case 'js': case 'jsx': return ; case 'json': return ; case 'png': case 'jpg': case 'jpeg': case 'gif': return ; default: return ; } }; if (!projectPath) { return (

{t.title}

{t.noProject}

); } const breadcrumbs = getBreadcrumbs(); return (
setSearchQuery(e.target.value)} style={{ flex: 1, padding: '6px 10px', background: '#3c3c3c', border: '1px solid #3e3e3e', borderRadius: '3px', color: '#cccccc', fontSize: '12px', outline: 'none' }} />
{showDetailView ? (
} rightOrBottom={
{breadcrumbs.map((crumb, index) => ( { setCurrentPath(crumb.path); loadAssets(crumb.path); }} > {crumb.name} {index < breadcrumbs.length - 1 && / } ))}
{(loading || isSearching) ? (

{isSearching ? '搜索中...' : t.loading}

) : filteredAssets.length === 0 ? (

{searchQuery.trim() ? '未找到匹配的资产' : t.empty}

) : (
{filteredAssets.map((asset, index) => { const relativePath = getRelativePath(asset.path); const showPath = searchQuery.trim() && relativePath; return (
handleAssetClick(asset)} onDoubleClick={() => handleAssetDoubleClick(asset)} onContextMenu={(e) => handleContextMenu(e, asset)} > {getFileIcon(asset)}
{asset.name}
{showPath && (
{relativePath}
)}
{asset.type === 'folder' ? t.folder : (asset.extension || t.file)}
); })}
)}
} /> ) : (
)}
{contextMenu && ( setContextMenu(null)} /> )} {/* 重命名对话框 */} {renameDialog && (
setRenameDialog(null)}>
e.stopPropagation()}>

{locale === 'zh' ? '重命名' : 'Rename'}

setRenameDialog({ ...renameDialog, newName: e.target.value })} onKeyDown={(e) => { if (e.key === 'Enter') { handleRename(renameDialog.asset, renameDialog.newName); } else if (e.key === 'Escape') { setRenameDialog(null); } }} autoFocus style={{ width: '100%', padding: '8px', backgroundColor: '#2d2d2d', border: '1px solid #3e3e3e', borderRadius: '4px', color: '#cccccc', fontSize: '13px' }} />
)} {/* 删除确认对话框 */} {deleteConfirmDialog && (
setDeleteConfirmDialog(null)}>
e.stopPropagation()}>

{locale === 'zh' ? '确认删除' : 'Confirm Delete'}

{locale === 'zh' ? `确定要删除 "${deleteConfirmDialog.name}" 吗?此操作不可撤销。` : `Are you sure you want to delete "${deleteConfirmDialog.name}"? This action cannot be undone.`}

)} ); }