fix: 修复项目切换时运行时和系统重复初始化问题 (#267)
* feat(editor): 添加 GitHub Discussions 社区论坛功能 * chore: 更新 pnpm-lock.yaml * chore: 删除测试图片 * refactor: 改用 Imgur 图床上传图片 * fix: 修复项目切换时运行时和系统重复初始化问题 * fix: 修复多个编辑器问题 * feat: 添加脚本编辑器配置和类型定义支持 * feat: 实现资产 .meta 文件自动生成功能
This commit is contained in:
@@ -40,6 +40,7 @@ import {
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
import { PromptDialog } from './PromptDialog';
|
||||
import '../styles/ContentBrowser.css';
|
||||
@@ -210,8 +211,124 @@ export function ContentBrowser({
|
||||
'Shader': { en: 'Shader', zh: '着色器' },
|
||||
'Tilemap': { en: 'Tilemap', zh: '瓦片地图' },
|
||||
'Tileset': { en: 'Tileset', zh: '瓦片集' },
|
||||
'Component': { en: 'Component', zh: '组件' },
|
||||
'System': { en: 'System', zh: '系统' },
|
||||
'TypeScript': { en: 'TypeScript', zh: 'TypeScript' },
|
||||
};
|
||||
|
||||
// 注册内置的 TypeScript 文件创建模板
|
||||
// Register built-in TypeScript file creation templates
|
||||
useEffect(() => {
|
||||
if (!fileActionRegistry) return;
|
||||
|
||||
const builtinTemplates: FileCreationTemplate[] = [
|
||||
{
|
||||
id: 'ts-component',
|
||||
label: 'Component',
|
||||
extension: '.ts',
|
||||
icon: 'FileCode',
|
||||
category: 'Script',
|
||||
getContent: (fileName: string) => {
|
||||
const className = fileName.replace(/\.ts$/, '');
|
||||
return `import { Component, ECSComponent, Property, Serialize, Serializable } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* ${className}
|
||||
*/
|
||||
@ECSComponent('${className}')
|
||||
@Serializable({ version: 1, typeId: '${className}' })
|
||||
export class ${className} extends Component {
|
||||
// 在这里添加组件属性
|
||||
// Add component properties here
|
||||
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Example Property' })
|
||||
public exampleProperty: number = 0;
|
||||
|
||||
onInitialize(): void {
|
||||
// 组件初始化时调用
|
||||
// Called when component is initialized
|
||||
}
|
||||
|
||||
onDestroy(): void {
|
||||
// 组件销毁时调用
|
||||
// Called when component is destroyed
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ts-system',
|
||||
label: 'System',
|
||||
extension: '.ts',
|
||||
icon: 'FileCode',
|
||||
category: 'Script',
|
||||
getContent: (fileName: string) => {
|
||||
const className = fileName.replace(/\.ts$/, '');
|
||||
return `import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* ${className}
|
||||
*/
|
||||
export class ${className} extends EntitySystem {
|
||||
// 定义系统处理的组件类型
|
||||
// Define component types this system processes
|
||||
protected getMatcher(): Matcher {
|
||||
// 返回匹配器,指定需要哪些组件
|
||||
// Return matcher specifying required components
|
||||
// return Matcher.all(SomeComponent);
|
||||
return Matcher.empty();
|
||||
}
|
||||
|
||||
protected updateEntity(entity: Entity, deltaTime: number): void {
|
||||
// 处理每个实体
|
||||
// Process each entity
|
||||
}
|
||||
|
||||
// 可选:系统初始化
|
||||
// Optional: System initialization
|
||||
// onInitialize(): void {
|
||||
// super.onInitialize();
|
||||
// }
|
||||
}
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'ts-script',
|
||||
label: 'TypeScript',
|
||||
extension: '.ts',
|
||||
icon: 'FileCode',
|
||||
category: 'Script',
|
||||
getContent: (fileName: string) => {
|
||||
const name = fileName.replace(/\.ts$/, '');
|
||||
return `/**
|
||||
* ${name}
|
||||
*/
|
||||
|
||||
export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
// 在这里编写代码
|
||||
// Write your code here
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// 注册模板
|
||||
for (const template of builtinTemplates) {
|
||||
fileActionRegistry.registerCreationTemplate(template);
|
||||
}
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
for (const template of builtinTemplates) {
|
||||
fileActionRegistry.unregisterCreationTemplate(template);
|
||||
}
|
||||
};
|
||||
}, [fileActionRegistry]);
|
||||
|
||||
const getTemplateLabel = (label: string): string => {
|
||||
const mapping = templateLabels[label];
|
||||
if (mapping) {
|
||||
@@ -439,6 +556,24 @@ export function ContentBrowser({
|
||||
return;
|
||||
}
|
||||
|
||||
// 脚本文件使用配置的编辑器打开
|
||||
// Open script files with configured editor
|
||||
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
|
||||
const settings = SettingsService.getInstance();
|
||||
const editorCommand = settings.getScriptEditorCommand();
|
||||
|
||||
if (editorCommand && projectPath) {
|
||||
try {
|
||||
await TauriAPI.openWithEditor(projectPath, editorCommand, asset.path);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to open with editor:', error);
|
||||
// 如果失败,回退到系统默认应用
|
||||
// Fall back to system default app if failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileActionRegistry) {
|
||||
const handled = await fileActionRegistry.handleDoubleClick(asset.path);
|
||||
if (handled) return;
|
||||
@@ -450,7 +585,7 @@ export function ContentBrowser({
|
||||
console.error('Failed to open file:', error);
|
||||
}
|
||||
}
|
||||
}, [loadAssets, onOpenScene, fileActionRegistry]);
|
||||
}, [loadAssets, onOpenScene, fileActionRegistry, projectPath]);
|
||||
|
||||
// Handle context menu
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => {
|
||||
@@ -799,9 +934,10 @@ export function ContentBrowser({
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
console.log('[ContentBrowser] showInFolder path:', asset.path);
|
||||
await TauriAPI.showInFolder(asset.path);
|
||||
} catch (error) {
|
||||
console.error('Failed to show in folder:', error);
|
||||
console.error('Failed to show in folder:', error, 'Path:', asset.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1126,10 +1262,16 @@ export function ContentBrowser({
|
||||
)}
|
||||
|
||||
{/* Create File Dialog */}
|
||||
{createFileDialog && (
|
||||
{createFileDialog && (() => {
|
||||
// 规范化扩展名(确保有点号前缀)
|
||||
// Normalize extension (ensure dot prefix)
|
||||
const ext = createFileDialog.template.extension.startsWith('.')
|
||||
? createFileDialog.template.extension
|
||||
: `.${createFileDialog.template.extension}`;
|
||||
return (
|
||||
<PromptDialog
|
||||
title={`New ${createFileDialog.template.label}`}
|
||||
message={`Enter file name (.${createFileDialog.template.extension} will be added):`}
|
||||
title={locale === 'zh' ? `新建 ${getTemplateLabel(createFileDialog.template.label)}` : `New ${createFileDialog.template.label}`}
|
||||
message={locale === 'zh' ? `输入文件名(将添加 ${ext}):` : `Enter file name (${ext} will be added):`}
|
||||
placeholder="filename"
|
||||
confirmText={locale === 'zh' ? '创建' : 'Create'}
|
||||
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
|
||||
@@ -1138,8 +1280,8 @@ export function ContentBrowser({
|
||||
setCreateFileDialog(null);
|
||||
|
||||
let fileName = value;
|
||||
if (!fileName.endsWith(`.${template.extension}`)) {
|
||||
fileName = `${fileName}.${template.extension}`;
|
||||
if (!fileName.endsWith(ext)) {
|
||||
fileName = `${fileName}${ext}`;
|
||||
}
|
||||
const filePath = `${parentPath}/${fileName}`;
|
||||
|
||||
@@ -1155,7 +1297,8 @@ export function ContentBrowser({
|
||||
}}
|
||||
onCancel={() => setCreateFileDialog(null)}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -833,9 +833,10 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
icon: <FolderOpen size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
console.log('[FileTree] showInFolder path:', node.path);
|
||||
await TauriAPI.showInFolder(node.path);
|
||||
} catch (error) {
|
||||
console.error('Failed to show in folder:', error);
|
||||
console.error('Failed to show in folder:', error, 'Path:', node.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { Globe, ChevronDown, Download, X, Loader2 } from 'lucide-react';
|
||||
import { Globe, ChevronDown, Download, X, Loader2, Trash2 } from 'lucide-react';
|
||||
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
||||
import { StartupLogo } from './StartupLogo';
|
||||
import '../styles/StartupPage.css';
|
||||
@@ -11,6 +11,8 @@ interface StartupPageProps {
|
||||
onOpenProject: () => void;
|
||||
onCreateProject: () => void;
|
||||
onOpenRecentProject?: (projectPath: string) => void;
|
||||
onRemoveRecentProject?: (projectPath: string) => void;
|
||||
onDeleteProject?: (projectPath: string) => Promise<void>;
|
||||
onLocaleChange?: (locale: Locale) => void;
|
||||
recentProjects?: string[];
|
||||
locale: string;
|
||||
@@ -21,11 +23,13 @@ const LANGUAGES = [
|
||||
{ code: 'zh', name: '中文' }
|
||||
];
|
||||
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
const [showLogo, setShowLogo] = useState(true);
|
||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
const [showLangMenu, setShowLangMenu] = useState(false);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; project: string } | null>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null);
|
||||
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
@@ -66,7 +70,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
updateAvailable: 'New version available',
|
||||
updateNow: 'Update Now',
|
||||
installing: 'Installing...',
|
||||
later: 'Later'
|
||||
later: 'Later',
|
||||
removeFromList: 'Remove from List',
|
||||
deleteProject: 'Delete Project',
|
||||
deleteConfirmTitle: 'Delete Project',
|
||||
deleteConfirmMessage: 'Are you sure you want to permanently delete this project? This action cannot be undone.',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete'
|
||||
},
|
||||
zh: {
|
||||
title: 'ESEngine 编辑器',
|
||||
@@ -78,7 +88,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
updateAvailable: '发现新版本',
|
||||
updateNow: '立即更新',
|
||||
installing: '正在安装...',
|
||||
later: '稍后'
|
||||
later: '稍后',
|
||||
removeFromList: '从列表中移除',
|
||||
deleteProject: '删除项目',
|
||||
deleteConfirmTitle: '删除项目',
|
||||
deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
delete: '删除'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,6 +152,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
onMouseEnter={() => setHoveredProject(project)}
|
||||
onMouseLeave={() => setHoveredProject(null)}
|
||||
onClick={() => onOpenRecentProject?.(project)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, project });
|
||||
}}
|
||||
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
|
||||
>
|
||||
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
@@ -145,6 +165,18 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
|
||||
<div className="recent-path">{project}</div>
|
||||
</div>
|
||||
{onRemoveRecentProject && (
|
||||
<button
|
||||
className="recent-remove-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveRecentProject(project);
|
||||
}}
|
||||
title={t.removeFromList}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -217,6 +249,78 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 | Context Menu */}
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="startup-context-menu-overlay"
|
||||
onClick={() => setContextMenu(null)}
|
||||
>
|
||||
<div
|
||||
className="startup-context-menu"
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="startup-context-menu-item"
|
||||
onClick={() => {
|
||||
onRemoveRecentProject?.(contextMenu.project);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
<span>{t.removeFromList}</span>
|
||||
</button>
|
||||
{onDeleteProject && (
|
||||
<button
|
||||
className="startup-context-menu-item danger"
|
||||
onClick={() => {
|
||||
setDeleteConfirm(contextMenu.project);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>{t.deleteProject}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 删除确认对话框 | Delete Confirmation Dialog */}
|
||||
{deleteConfirm && (
|
||||
<div className="startup-dialog-overlay">
|
||||
<div className="startup-dialog">
|
||||
<div className="startup-dialog-header">
|
||||
<Trash2 size={20} className="dialog-icon-danger" />
|
||||
<h3>{t.deleteConfirmTitle}</h3>
|
||||
</div>
|
||||
<div className="startup-dialog-body">
|
||||
<p>{t.deleteConfirmMessage}</p>
|
||||
<p className="startup-dialog-path">{deleteConfirm}</p>
|
||||
</div>
|
||||
<div className="startup-dialog-footer">
|
||||
<button
|
||||
className="startup-dialog-btn"
|
||||
onClick={() => setDeleteConfirm(null)}
|
||||
>
|
||||
{t.cancel}
|
||||
</button>
|
||||
<button
|
||||
className="startup-dialog-btn danger"
|
||||
onClick={async () => {
|
||||
if (deleteConfirm && onDeleteProject) {
|
||||
await onDeleteProject(deleteConfirm);
|
||||
}
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
>
|
||||
{t.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user