feat(i18n): 统一国际化系统架构,支持插件独立翻译 (#301)
* feat(i18n): 统一国际化系统架构,支持插件独立翻译 ## 主要改动 ### 核心架构 - 增强 LocaleService,支持插件命名空间翻译扩展 - 新增 editor-runtime/i18n 模块,提供 createPluginLocale/createPluginTranslator - 新增 editor-core/tokens.ts,定义 LocaleServiceToken 等服务令牌 - 改进 PluginAPI 类型安全,使用 ServiceToken<T> 替代 any ### 编辑器本地化 - 扩展 en.ts/zh.ts 翻译文件,覆盖所有 UI 组件 - 新增 es.ts 西班牙语支持 - 重构 40+ 组件使用 useLocale() hook ### 插件本地化系统 - behavior-tree-editor: 新增 locales/ 和 useBTLocale hook - material-editor: 新增 locales/ 和 useMaterialLocale hook - particle-editor: 新增 locales/ 和 useParticleLocale hook - tilemap-editor: 新增 locales/ 和 useTilemapLocale hook - ui-editor: 新增 locales/ 和 useUILocale hook ### 类型安全改进 - 修复 Debug 工具使用公共接口替代 as any - 修复 ChunkStreamingSystem 添加 forEachChunk 公共方法 - 修复 blueprint-editor 移除不必要的向后兼容代码 * fix(behavior-tree-editor): 使用 ServiceToken 模式修复服务解析 - 创建 BehaviorTreeServiceToken 遵循"谁定义接口,谁导出Token"原则 - 使用 ServiceToken.id (symbol) 注册服务到 ServiceContainer - 更新 PluginSDKRegistry.resolveService 支持 ServiceToken 检测 - BehaviorTreeEditorPanel 现在使用类型安全的 PluginAPI.resolve * fix(behavior-tree-editor): 使用 ServiceContainer.resolve 获取类注册的服务 * fix: 修复多个包的依赖和类型问题 - core: EntityDataCollector.getEntityDetails 使用 HierarchySystem 获取父实体 - ui-editor: 添加 @esengine/editor-runtime 依赖 - tilemap-editor: 添加 @esengine/editor-runtime 依赖 - particle-editor: 添加 @esengine/editor-runtime 依赖
This commit is contained in:
@@ -58,7 +58,7 @@ import { EngineService } from './services/EngineService';
|
||||
import { CompilerConfigDialog } from './components/CompilerConfigDialog';
|
||||
import { checkForUpdatesOnStartup } from './utils/updater';
|
||||
import { useLocale } from './hooks/useLocale';
|
||||
import { en, zh } from './locales';
|
||||
import { en, zh, es } from './locales';
|
||||
import type { Locale } from '@esengine/editor-core';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import './styles/App.css';
|
||||
@@ -68,6 +68,7 @@ const coreInstance = Core.create({ debug: true });
|
||||
const localeService = new LocaleService();
|
||||
localeService.registerTranslations('en', en);
|
||||
localeService.registerTranslations('zh', zh);
|
||||
localeService.registerTranslations('es', es);
|
||||
Core.services.registerInstance(LocaleService, localeService);
|
||||
|
||||
Core.services.registerSingleton(GlobalBlackboardService);
|
||||
@@ -161,10 +162,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.saveScene();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
showToast(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`, 'success');
|
||||
showToast(t('scene.savedSuccess', { name: sceneState.sceneName }), 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene:', error);
|
||||
showToast(locale === 'zh' ? '保存场景失败' : 'Failed to save scene', 'error');
|
||||
showToast(t('scene.saveFailed'), 'error');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -366,7 +367,7 @@ function App() {
|
||||
const handleOpenRecentProject = async (projectPath: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 1/3: 打开项目配置...' : 'Step 1/3: Opening project config...');
|
||||
setLoadingMessage(t('loading.step1'));
|
||||
|
||||
const projectService = Core.services.resolve(ProjectService);
|
||||
|
||||
@@ -400,13 +401,13 @@ function App() {
|
||||
setProjectLoaded(true);
|
||||
|
||||
// 等待引擎初始化完成(Viewport 渲染后会触发引擎初始化)
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 2/3: 初始化引擎和模块...' : 'Step 2/3: Initializing engine and modules...');
|
||||
setLoadingMessage(t('loading.step2'));
|
||||
const engineService = EngineService.getInstance();
|
||||
|
||||
// 等待引擎初始化(最多等待 30 秒,因为需要等待 Viewport 渲染)
|
||||
const engineReady = await engineService.waitForInitialization(30000);
|
||||
if (!engineReady) {
|
||||
throw new Error(locale === 'zh' ? '引擎初始化超时' : 'Engine initialization timeout');
|
||||
throw new Error(t('loading.engineTimeoutError'));
|
||||
}
|
||||
|
||||
// 加载项目插件配置并激活插件(在引擎初始化后、模块系统初始化前)
|
||||
@@ -432,7 +433,7 @@ function App() {
|
||||
|
||||
setStatus(t('header.status.projectOpened'));
|
||||
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 3/3: 初始化场景...' : 'Step 3/3: Initializing scene...');
|
||||
setLoadingMessage(t('loading.step3'));
|
||||
|
||||
const sceneManagerService = Core.services.resolve(SceneManagerService);
|
||||
if (sceneManagerService) {
|
||||
@@ -440,7 +441,7 @@ function App() {
|
||||
}
|
||||
|
||||
if (pluginManager) {
|
||||
setLoadingMessage(locale === 'zh' ? '加载项目插件...' : 'Loading project plugins...');
|
||||
setLoadingMessage(t('loading.loadingPlugins'));
|
||||
await pluginLoader.loadProjectPlugins(projectPath, pluginManager);
|
||||
}
|
||||
|
||||
@@ -452,10 +453,8 @@ function App() {
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '打开项目失败' : 'Failed to Open Project',
|
||||
message: locale === 'zh'
|
||||
? `无法打开项目:\n${errorMessage}`
|
||||
: `Failed to open project:\n${errorMessage}`
|
||||
title: t('project.openFailed'),
|
||||
message: `${t('project.openFailed')}:\n${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -482,22 +481,22 @@ function App() {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(locale === 'zh' ? '正在创建项目...' : 'Creating project...');
|
||||
setLoadingMessage(t('project.creating'));
|
||||
|
||||
const projectService = Core.services.resolve(ProjectService);
|
||||
if (!projectService) {
|
||||
console.error('ProjectService not available');
|
||||
setIsLoading(false);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '创建项目失败' : 'Failed to Create Project',
|
||||
message: locale === 'zh' ? '项目服务不可用,请重启编辑器' : 'Project service is not available. Please restart the editor.'
|
||||
title: t('project.createFailed'),
|
||||
message: t('project.serviceUnavailable')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await projectService.createProject(fullProjectPath);
|
||||
|
||||
setLoadingMessage(locale === 'zh' ? '项目创建成功,正在打开...' : 'Project created, opening...');
|
||||
setLoadingMessage(t('project.createdOpening'));
|
||||
|
||||
await handleOpenRecentProject(fullProjectPath);
|
||||
} catch (error) {
|
||||
@@ -508,35 +507,29 @@ function App() {
|
||||
|
||||
if (errorMessage.includes('already exists')) {
|
||||
setConfirmDialog({
|
||||
title: locale === 'zh' ? '项目已存在' : 'Project Already Exists',
|
||||
message: locale === 'zh'
|
||||
? '该目录下已存在 ECS 项目,是否要打开该项目?'
|
||||
: 'An ECS project already exists in this directory. Do you want to open it?',
|
||||
confirmText: locale === 'zh' ? '打开项目' : 'Open Project',
|
||||
cancelText: locale === 'zh' ? '取消' : 'Cancel',
|
||||
title: t('project.alreadyExists'),
|
||||
message: t('project.existsQuestion'),
|
||||
confirmText: t('project.open'),
|
||||
cancelText: t('common.cancel'),
|
||||
onConfirm: () => {
|
||||
setConfirmDialog(null);
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(locale === 'zh' ? '正在打开项目...' : 'Opening project...');
|
||||
setLoadingMessage(t('project.opening'));
|
||||
handleOpenRecentProject(fullProjectPath).catch((err) => {
|
||||
console.error('Failed to open project:', err);
|
||||
setIsLoading(false);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '打开项目失败' : 'Failed to Open Project',
|
||||
message: locale === 'zh'
|
||||
? `无法打开项目:\n${err instanceof Error ? err.message : String(err)}`
|
||||
: `Failed to open project:\n${err instanceof Error ? err.message : String(err)}`
|
||||
title: t('project.openFailed'),
|
||||
message: `${t('project.openFailed')}:\n${err instanceof Error ? err.message : String(err)}`
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStatus(locale === 'zh' ? '创建项目失败' : 'Failed to create project');
|
||||
setStatus(t('project.createFailed'));
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '创建项目失败' : 'Failed to Create Project',
|
||||
message: locale === 'zh'
|
||||
? `无法创建项目:\n${errorMessage}`
|
||||
: `Failed to create project:\n${errorMessage}`
|
||||
title: t('project.createFailed'),
|
||||
message: `${t('project.createFailed')}:\n${errorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -560,10 +553,10 @@ function App() {
|
||||
|
||||
try {
|
||||
await sceneManager.newScene();
|
||||
setStatus(locale === 'zh' ? '已创建新场景' : 'New scene created');
|
||||
setStatus(t('scene.newCreated'));
|
||||
} catch (error) {
|
||||
console.error('Failed to create new scene:', error);
|
||||
setStatus(locale === 'zh' ? '创建场景失败' : 'Failed to create scene');
|
||||
setStatus(t('scene.createFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -576,10 +569,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.openScene();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已打开场景: ${sceneState.sceneName}` : `Scene opened: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to open scene:', error);
|
||||
setStatus(locale === 'zh' ? '打开场景失败' : 'Failed to open scene');
|
||||
setStatus(t('scene.openFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -592,15 +585,13 @@ function App() {
|
||||
try {
|
||||
await sceneManager.openScene(scenePath);
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已打开场景: ${sceneState.sceneName}` : `Scene opened: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to open scene:', error);
|
||||
setStatus(locale === 'zh' ? '打开场景失败' : 'Failed to open scene');
|
||||
setStatus(t('scene.openFailed'));
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '打开场景失败' : 'Failed to Open Scene',
|
||||
message: locale === 'zh'
|
||||
? `无法打开场景:\n${error instanceof Error ? error.message : String(error)}`
|
||||
: `Failed to open scene:\n${error instanceof Error ? error.message : String(error)}`
|
||||
title: t('scene.openFailed'),
|
||||
message: `${t('scene.openFailed')}:\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}, [sceneManager, locale]);
|
||||
@@ -615,10 +606,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.saveScene();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene:', error);
|
||||
setStatus(locale === 'zh' ? '保存场景失败' : 'Failed to save scene');
|
||||
setStatus(t('scene.saveFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -631,10 +622,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.saveSceneAs();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene as:', error);
|
||||
setStatus(locale === 'zh' ? '另存场景失败' : 'Failed to save scene as');
|
||||
setStatus(t('scene.saveAsFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -726,10 +717,10 @@ function App() {
|
||||
// 7. 触发面板重新渲染
|
||||
setPluginUpdateTrigger((prev) => prev + 1);
|
||||
|
||||
showToast(locale === 'zh' ? '插件已重新加载' : 'Plugins reloaded', 'success');
|
||||
showToast(t('plugin.reloadedSuccess'), 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to reload plugins:', error);
|
||||
showToast(locale === 'zh' ? '重新加载插件失败' : 'Failed to reload plugins', 'error');
|
||||
showToast(t('plugin.reloadFailed'), 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -739,25 +730,25 @@ function App() {
|
||||
const corePanels: FlexDockPanel[] = [
|
||||
{
|
||||
id: 'scene-hierarchy',
|
||||
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
||||
title: t('panel.sceneHierarchy'),
|
||||
content: <SceneHierarchy entityStore={entityStore} messageHub={messageHub} commandManager={commandManager} />,
|
||||
closable: false
|
||||
},
|
||||
{
|
||||
id: 'viewport',
|
||||
title: locale === 'zh' ? '视口' : 'Viewport',
|
||||
title: t('panel.viewport'),
|
||||
content: <Viewport locale={locale} messageHub={messageHub} />,
|
||||
closable: false
|
||||
},
|
||||
{
|
||||
id: 'inspector',
|
||||
title: locale === 'zh' ? '检视器' : 'Inspector',
|
||||
title: t('panel.inspector'),
|
||||
content: <Inspector entityStore={entityStore} messageHub={messageHub} inspectorRegistry={inspectorRegistry!} projectPath={currentProjectPath} commandManager={commandManager} />,
|
||||
closable: false
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
title: locale === 'zh' ? '社区论坛' : 'Forum',
|
||||
title: t('panel.forum'),
|
||||
content: <ForumPanel />,
|
||||
closable: true
|
||||
}
|
||||
@@ -776,9 +767,12 @@ function App() {
|
||||
})
|
||||
.map((panelDesc) => {
|
||||
const Component = panelDesc.component;
|
||||
// 使用 titleKey 翻译,回退到 title
|
||||
// Use titleKey for translation, fallback to title
|
||||
const title = panelDesc.titleKey ? t(panelDesc.titleKey) : panelDesc.title;
|
||||
return {
|
||||
id: panelDesc.id,
|
||||
title: panelDesc.titleZh && locale === 'zh' ? panelDesc.titleZh : panelDesc.title,
|
||||
title,
|
||||
content: <Component key={`${panelDesc.id}-${pluginUpdateTrigger}`} projectPath={currentProjectPath} />,
|
||||
closable: panelDesc.closable ?? true
|
||||
};
|
||||
@@ -793,8 +787,10 @@ function App() {
|
||||
.map((panelId) => {
|
||||
const panelDesc = uiRegistry.getPanel(panelId)!;
|
||||
// 优先使用动态标题,否则使用默认标题
|
||||
// Prefer dynamic title, fallback to default title
|
||||
const customTitle = dynamicPanelTitles.get(panelId);
|
||||
const defaultTitle = panelDesc.titleZh && locale === 'zh' ? panelDesc.titleZh : panelDesc.title;
|
||||
// 使用 titleKey 翻译,回退到 title
|
||||
const defaultTitle = panelDesc.titleKey ? t(panelDesc.titleKey) : panelDesc.title;
|
||||
|
||||
// 支持 component 或 render 两种方式
|
||||
let content: React.ReactNode;
|
||||
@@ -855,16 +851,13 @@ function App() {
|
||||
} catch (error) {
|
||||
console.error('[App] Failed to delete project:', error);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '删除项目失败' : 'Failed to Delete Project',
|
||||
message: locale === 'zh'
|
||||
? `无法删除项目:\n${error instanceof Error ? error.message : String(error)}`
|
||||
: `Failed to delete project:\n${error instanceof Error ? error.message : String(error)}`
|
||||
title: t('project.deleteFailed'),
|
||||
message: `${t('project.deleteFailed')}:\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
recentProjects={recentProjects}
|
||||
locale={locale}
|
||||
/>
|
||||
<ProjectCreationWizard
|
||||
isOpen={showProjectWizard}
|
||||
@@ -918,7 +911,6 @@ function App() {
|
||||
<>
|
||||
<TitleBar
|
||||
projectName={projectName}
|
||||
locale={locale}
|
||||
uiRegistry={uiRegistry || undefined}
|
||||
messageHub={messageHub || undefined}
|
||||
pluginManager={pluginManager || undefined}
|
||||
@@ -943,7 +935,6 @@ function App() {
|
||||
onOpenBuildSettings={() => setShowBuildSettings(true)}
|
||||
/>
|
||||
<MainToolbar
|
||||
locale={locale}
|
||||
messageHub={messageHub || undefined}
|
||||
commandManager={commandManager}
|
||||
onSaveScene={handleSaveScene}
|
||||
@@ -1013,14 +1004,13 @@ function App() {
|
||||
)}
|
||||
|
||||
{showAbout && (
|
||||
<AboutDialog onClose={() => setShowAbout(false)} locale={locale} />
|
||||
<AboutDialog onClose={() => setShowAbout(false)} />
|
||||
)}
|
||||
|
||||
{showPluginGenerator && (
|
||||
<PluginGeneratorWindow
|
||||
onClose={() => setShowPluginGenerator(false)}
|
||||
projectPath={currentProjectPath}
|
||||
locale={locale}
|
||||
onSuccess={async () => {
|
||||
if (currentProjectPath && pluginManager) {
|
||||
await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManager);
|
||||
@@ -1033,7 +1023,6 @@ function App() {
|
||||
<BuildSettingsWindow
|
||||
onClose={() => setShowBuildSettings(false)}
|
||||
projectPath={currentProjectPath || undefined}
|
||||
locale={locale}
|
||||
buildService={buildService || undefined}
|
||||
sceneManager={sceneManager || undefined}
|
||||
/>
|
||||
|
||||
@@ -4,14 +4,15 @@ import { checkForUpdates, installUpdate } from '../utils/updater';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { MiniParticleLogo } from './MiniParticleLogo';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/AboutDialog.css';
|
||||
|
||||
interface AboutDialogProps {
|
||||
onClose: () => void;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
export function AboutDialog({ onClose }: AboutDialogProps) {
|
||||
const { t } = useLocale();
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'latest' | 'error' | 'installing'>('idle');
|
||||
@@ -31,44 +32,6 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
fetchVersion();
|
||||
}, []);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
title: 'About ESEngine Editor',
|
||||
version: 'Version',
|
||||
description: 'High-performance game editor for ECS-based game development',
|
||||
checkUpdate: 'Check for Updates',
|
||||
checking: 'Checking...',
|
||||
updateAvailable: 'New version available',
|
||||
latest: 'You are using the latest version',
|
||||
error: 'Failed to check for updates',
|
||||
download: 'Download & Install',
|
||||
installing: 'Installing...',
|
||||
close: 'Close',
|
||||
copyright: '© 2025 ESEngine. All rights reserved.',
|
||||
website: 'Website',
|
||||
github: 'GitHub'
|
||||
},
|
||||
zh: {
|
||||
title: '关于 ESEngine Editor',
|
||||
version: '版本',
|
||||
description: '高性能游戏编辑器,基于 ECS 架构',
|
||||
checkUpdate: '检查更新',
|
||||
checking: '检查中...',
|
||||
updateAvailable: '发现新版本',
|
||||
latest: '您正在使用最新版本',
|
||||
error: '检查更新失败',
|
||||
download: '下载并安装',
|
||||
installing: '正在安装...',
|
||||
close: '关闭',
|
||||
copyright: '© 2025 ESEngine. 保留所有权利。',
|
||||
website: '官网',
|
||||
github: 'GitHub'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
const handleCheckUpdate = async () => {
|
||||
setChecking(true);
|
||||
setUpdateStatus('checking');
|
||||
@@ -136,15 +99,15 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
const getStatusText = () => {
|
||||
switch (updateStatus) {
|
||||
case 'checking':
|
||||
return t('checking');
|
||||
return t('about.checking');
|
||||
case 'available':
|
||||
return `${t('updateAvailable')} (v${newVersion})`;
|
||||
return `${t('about.updateAvailable')} (v${newVersion})`;
|
||||
case 'installing':
|
||||
return t('installing');
|
||||
return t('about.installing');
|
||||
case 'latest':
|
||||
return t('latest');
|
||||
return t('about.latest');
|
||||
case 'error':
|
||||
return t('error');
|
||||
return t('about.error');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -162,7 +125,7 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="about-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="about-header">
|
||||
<h2>{t('title')}</h2>
|
||||
<h2>{t('about.title')}</h2>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -176,10 +139,10 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
<div className="about-info">
|
||||
<h3>ESEngine Editor</h3>
|
||||
<p className="about-version">
|
||||
{t('version')}: Editor {version}
|
||||
{t('about.version')}: Editor {version}
|
||||
</p>
|
||||
<p className="about-description">
|
||||
{t('description')}
|
||||
{t('about.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -192,12 +155,12 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
{checking ? (
|
||||
<>
|
||||
<RefreshCw size={16} className="animate-spin" />
|
||||
<span>{t('checking')}</span>
|
||||
<span>{t('about.checking')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw size={16} />
|
||||
<span>{t('checkUpdate')}</span>
|
||||
<span>{t('about.checkUpdate')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -218,12 +181,12 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
{installing ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>{t('installing')}</span>
|
||||
<span>{t('about.installing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download size={16} />
|
||||
<span>{t('download')}</span>
|
||||
<span>{t('about.download')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -239,18 +202,18 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
}}
|
||||
className="about-link"
|
||||
>
|
||||
{t('github')}
|
||||
{t('about.github')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="about-footer">
|
||||
<p>{t('copyright')}</p>
|
||||
<p>{t('about.copyright')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="about-actions">
|
||||
<button className="btn-primary" onClick={onClose}>
|
||||
{t('close')}
|
||||
{t('about.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/AssetPickerDialog.css';
|
||||
|
||||
interface AssetPickerDialogProps {
|
||||
@@ -25,6 +26,8 @@ interface AssetItem {
|
||||
type ViewMode = 'list' | 'grid';
|
||||
|
||||
export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) {
|
||||
const { t, locale: currentLocale } = useLocale();
|
||||
|
||||
// 计算实际的资产目录路径
|
||||
const actualAssetPath = assetBasePath
|
||||
? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/')
|
||||
@@ -37,33 +40,6 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'Select Asset',
|
||||
loading: 'Loading...',
|
||||
empty: 'No assets found',
|
||||
select: 'Select',
|
||||
cancel: 'Cancel',
|
||||
search: 'Search...',
|
||||
back: 'Back',
|
||||
listView: 'List View',
|
||||
gridView: 'Grid View'
|
||||
},
|
||||
zh: {
|
||||
title: '选择资产',
|
||||
loading: '加载中...',
|
||||
empty: '没有找到资产',
|
||||
select: '选择',
|
||||
cancel: '取消',
|
||||
search: '搜索...',
|
||||
back: '返回上级',
|
||||
listView: '列表视图',
|
||||
gridView: '网格视图'
|
||||
}
|
||||
};
|
||||
|
||||
const t = translations[locale as keyof typeof translations] || translations.en;
|
||||
|
||||
useEffect(() => {
|
||||
loadAssets(currentPath);
|
||||
}, [currentPath]);
|
||||
@@ -118,7 +94,8 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
const formatDate = (timestamp?: number): string => {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
const localeMap: Record<string, string> = { zh: 'zh-CN', en: 'en-US', es: 'es-ES' };
|
||||
return date.toLocaleDateString(localeMap[currentLocale] || 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -213,7 +190,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
<div className="asset-picker-overlay" onClick={onClose}>
|
||||
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="asset-picker-header">
|
||||
<h3>{t.title}</h3>
|
||||
<h3>{t('assetPicker.title')}</h3>
|
||||
<button className="asset-picker-close" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -224,7 +201,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
className="toolbar-button"
|
||||
onClick={handleGoBack}
|
||||
disabled={!canGoBack}
|
||||
title={t.back}
|
||||
title={t('assetPicker.back')}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
@@ -247,14 +224,14 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
<button
|
||||
className={`toolbar-button ${viewMode === 'list' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('list')}
|
||||
title={t.listView}
|
||||
title={t('assetPicker.listView')}
|
||||
>
|
||||
<List size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={`toolbar-button ${viewMode === 'grid' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('grid')}
|
||||
title={t.gridView}
|
||||
title={t('assetPicker.gridView')}
|
||||
>
|
||||
<Grid size={16} />
|
||||
</button>
|
||||
@@ -265,7 +242,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
<Search size={16} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.search}
|
||||
placeholder={t('assetPicker.search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
@@ -282,9 +259,9 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
|
||||
<div className="asset-picker-content">
|
||||
{loading ? (
|
||||
<div className="asset-picker-loading">{t.loading}</div>
|
||||
<div className="asset-picker-loading">{t('assetPicker.loading')}</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="asset-picker-empty">{t.empty}</div>
|
||||
<div className="asset-picker-empty">{t('assetPicker.empty')}</div>
|
||||
) : (
|
||||
<div className={`asset-picker-list ${viewMode}`}>
|
||||
{filteredAssets.map((item, index) => (
|
||||
@@ -318,18 +295,18 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
|
||||
<div className="asset-picker-footer">
|
||||
<div className="footer-info">
|
||||
{filteredAssets.length} {locale === 'zh' ? '项' : 'items'}
|
||||
{t('assetPicker.itemCount', { count: filteredAssets.length })}
|
||||
</div>
|
||||
<div className="footer-buttons">
|
||||
<button className="asset-picker-cancel" onClick={onClose}>
|
||||
{t.cancel}
|
||||
{t('assetPicker.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="asset-picker-select"
|
||||
onClick={handleSelect}
|
||||
disabled={!selectedPath}
|
||||
>
|
||||
{t.select}
|
||||
{t('assetPicker.select')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { BuildService, BuildProgress, BuildConfig, WebBuildConfig, WeChatBuildConfig, SceneManagerService } from '@esengine/editor-core';
|
||||
import { BuildPlatform, BuildStatus } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/BuildSettingsPanel.css';
|
||||
|
||||
// ==================== Types | 类型定义 ====================
|
||||
@@ -89,110 +90,25 @@ const DEFAULT_SETTINGS: BuildSettings = {
|
||||
bundleModules: false,
|
||||
};
|
||||
|
||||
// ==================== i18n | 国际化 ====================
|
||||
// ==================== Status Key Mapping | 状态键映射 ====================
|
||||
|
||||
const i18n = {
|
||||
en: {
|
||||
buildProfiles: 'Build Profiles',
|
||||
addBuildProfile: 'Add Build Profile',
|
||||
playerSettings: 'Player Settings',
|
||||
assetImportOverrides: 'Asset Import Overrides',
|
||||
platforms: 'Platforms',
|
||||
sceneList: 'Scene List',
|
||||
active: 'Active',
|
||||
switchProfile: 'Switch Profile',
|
||||
build: 'Build',
|
||||
buildAndRun: 'Build And Run',
|
||||
buildData: 'Build Data',
|
||||
scriptingDefines: 'Scripting Defines',
|
||||
listIsEmpty: 'List is empty',
|
||||
addOpenScenes: 'Add Open Scenes',
|
||||
platformSettings: 'Platform Settings',
|
||||
architecture: 'Architecture',
|
||||
developmentBuild: 'Development Build',
|
||||
sourceMap: 'Source Map',
|
||||
compressionMethod: 'Compression Method',
|
||||
bundleModules: 'Bundle Modules',
|
||||
bundleModulesHint: 'Merge all modules into single file',
|
||||
separateModulesHint: 'Keep modules as separate files',
|
||||
playerSettingsOverrides: 'Player Settings Overrides',
|
||||
companyName: 'Company Name',
|
||||
productName: 'Product Name',
|
||||
version: 'Version',
|
||||
defaultIcon: 'Default Icon',
|
||||
none: 'None',
|
||||
// Build progress | 构建进度
|
||||
buildInProgress: 'Build in Progress',
|
||||
preparing: 'Preparing...',
|
||||
compiling: 'Compiling...',
|
||||
packaging: 'Packaging assets...',
|
||||
copying: 'Copying files...',
|
||||
postProcessing: 'Post-processing...',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
buildSucceeded: 'Build succeeded!',
|
||||
buildFailed: 'Build failed',
|
||||
warnings: 'Warnings',
|
||||
outputPath: 'Output Path',
|
||||
duration: 'Duration',
|
||||
},
|
||||
zh: {
|
||||
buildProfiles: '构建配置',
|
||||
addBuildProfile: '添加构建配置',
|
||||
playerSettings: '玩家设置',
|
||||
assetImportOverrides: '资源导入覆盖',
|
||||
platforms: '平台',
|
||||
sceneList: '场景列表',
|
||||
active: '激活',
|
||||
switchProfile: '切换配置',
|
||||
build: '构建',
|
||||
buildAndRun: '构建并运行',
|
||||
buildData: '构建数据',
|
||||
scriptingDefines: '脚本定义',
|
||||
listIsEmpty: '列表为空',
|
||||
addOpenScenes: '添加已打开的场景',
|
||||
platformSettings: '平台设置',
|
||||
architecture: '架构',
|
||||
developmentBuild: '开发版本',
|
||||
sourceMap: 'Source Map',
|
||||
compressionMethod: '压缩方式',
|
||||
bundleModules: '打包模块',
|
||||
bundleModulesHint: '合并所有模块为单文件',
|
||||
separateModulesHint: '保持模块为独立文件',
|
||||
playerSettingsOverrides: '玩家设置覆盖',
|
||||
companyName: '公司名称',
|
||||
productName: '产品名称',
|
||||
version: '版本',
|
||||
defaultIcon: '默认图标',
|
||||
none: '无',
|
||||
// Build progress | 构建进度
|
||||
buildInProgress: '正在构建',
|
||||
preparing: '准备中...',
|
||||
compiling: '编译中...',
|
||||
packaging: '打包资源...',
|
||||
copying: '复制文件...',
|
||||
postProcessing: '后处理...',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
buildSucceeded: '构建成功!',
|
||||
buildFailed: '构建失败',
|
||||
warnings: '警告',
|
||||
outputPath: '输出路径',
|
||||
duration: '耗时',
|
||||
}
|
||||
/** Map BuildStatus to translation key | 将 BuildStatus 映射到翻译键 */
|
||||
const buildStatusKeys: Record<BuildStatus, string> = {
|
||||
[BuildStatus.Idle]: 'buildSettings.preparing',
|
||||
[BuildStatus.Preparing]: 'buildSettings.preparing',
|
||||
[BuildStatus.Compiling]: 'buildSettings.compiling',
|
||||
[BuildStatus.Packaging]: 'buildSettings.packaging',
|
||||
[BuildStatus.Copying]: 'buildSettings.copying',
|
||||
[BuildStatus.PostProcessing]: 'buildSettings.postProcessing',
|
||||
[BuildStatus.Completed]: 'buildSettings.completed',
|
||||
[BuildStatus.Failed]: 'buildSettings.failed',
|
||||
[BuildStatus.Cancelled]: 'buildSettings.cancelled'
|
||||
};
|
||||
|
||||
// ==================== Props | 属性 ====================
|
||||
|
||||
interface BuildSettingsPanelProps {
|
||||
projectPath?: string;
|
||||
locale?: string;
|
||||
buildService?: BuildService;
|
||||
sceneManager?: SceneManagerService;
|
||||
onBuild?: (profile: BuildProfile, settings: BuildSettings) => void;
|
||||
@@ -203,13 +119,12 @@ interface BuildSettingsPanelProps {
|
||||
|
||||
export function BuildSettingsPanel({
|
||||
projectPath,
|
||||
locale = 'en',
|
||||
buildService,
|
||||
sceneManager,
|
||||
onBuild,
|
||||
onClose
|
||||
}: BuildSettingsPanelProps) {
|
||||
const t = i18n[locale as keyof typeof i18n] || i18n.en;
|
||||
const { t } = useLocale();
|
||||
|
||||
// State | 状态
|
||||
const [profiles, setProfiles] = useState<BuildProfile[]>([
|
||||
@@ -397,18 +312,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
// Get status message | 获取状态消息
|
||||
const getStatusMessage = useCallback((status: BuildStatus): string => {
|
||||
const statusMessages: Record<BuildStatus, keyof typeof i18n.en> = {
|
||||
[BuildStatus.Idle]: 'preparing',
|
||||
[BuildStatus.Preparing]: 'preparing',
|
||||
[BuildStatus.Compiling]: 'compiling',
|
||||
[BuildStatus.Packaging]: 'packaging',
|
||||
[BuildStatus.Copying]: 'copying',
|
||||
[BuildStatus.PostProcessing]: 'postProcessing',
|
||||
[BuildStatus.Completed]: 'completed',
|
||||
[BuildStatus.Failed]: 'failed',
|
||||
[BuildStatus.Cancelled]: 'cancelled'
|
||||
};
|
||||
return t[statusMessages[status]] || status;
|
||||
return t(buildStatusKeys[status]) || status;
|
||||
}, [t]);
|
||||
|
||||
const handleAddScene = useCallback(() => {
|
||||
@@ -466,12 +370,12 @@ export function BuildSettingsPanel({
|
||||
<div className="build-settings-tabs">
|
||||
<div className="build-settings-tab active">
|
||||
<Package size={14} />
|
||||
{t.buildProfiles}
|
||||
{t('buildSettings.buildProfiles')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="build-settings-header-actions">
|
||||
<button className="build-settings-header-btn">{t.playerSettings}</button>
|
||||
<button className="build-settings-header-btn">{t.assetImportOverrides}</button>
|
||||
<button className="build-settings-header-btn">{t('buildSettings.playerSettings')}</button>
|
||||
<button className="build-settings-header-btn">{t('buildSettings.assetImportOverrides')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -479,7 +383,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-settings-add-bar">
|
||||
<button className="build-settings-add-btn" onClick={handleAddProfile}>
|
||||
<Plus size={14} />
|
||||
{t.addBuildProfile}
|
||||
{t('buildSettings.addBuildProfile')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -489,7 +393,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-settings-sidebar">
|
||||
{/* Platforms Section | 平台部分 */}
|
||||
<div className="build-settings-section">
|
||||
<div className="build-settings-section-header">{t.platforms}</div>
|
||||
<div className="build-settings-section-header">{t('buildSettings.platforms')}</div>
|
||||
<div className="build-settings-platform-list">
|
||||
{PLATFORMS.map(platform => {
|
||||
const isActive = profiles.some(p => p.platform === platform.platform && p.isActive);
|
||||
@@ -501,7 +405,7 @@ export function BuildSettingsPanel({
|
||||
>
|
||||
<span className="build-settings-platform-icon">{platform.icon}</span>
|
||||
<span className="build-settings-platform-label">{platform.label}</span>
|
||||
{isActive && <span className="build-settings-active-badge">{t.active}</span>}
|
||||
{isActive && <span className="build-settings-active-badge">{t('buildSettings.active')}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -510,7 +414,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
{/* Build Profiles Section | 构建配置部分 */}
|
||||
<div className="build-settings-section">
|
||||
<div className="build-settings-section-header">{t.buildProfiles}</div>
|
||||
<div className="build-settings-section-header">{t('buildSettings.buildProfiles')}</div>
|
||||
<div className="build-settings-profile-list">
|
||||
{profiles
|
||||
.filter(p => p.platform === selectedPlatform)
|
||||
@@ -546,9 +450,9 @@ export function BuildSettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
<div className="build-settings-details-actions">
|
||||
<button className="build-settings-btn secondary">{t.switchProfile}</button>
|
||||
<button className="build-settings-btn secondary">{t('buildSettings.switchProfile')}</button>
|
||||
<button className="build-settings-btn primary" onClick={handleBuild}>
|
||||
{t.build}
|
||||
{t('buildSettings.build')}
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -556,7 +460,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
{/* Build Data Section | 构建数据部分 */}
|
||||
<div className="build-settings-card">
|
||||
<div className="build-settings-card-header">{t.buildData}</div>
|
||||
<div className="build-settings-card-header">{t('buildSettings.buildData')}</div>
|
||||
|
||||
{/* Scene List | 场景列表 */}
|
||||
<div className="build-settings-field-group">
|
||||
@@ -565,7 +469,7 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('sceneList')}
|
||||
>
|
||||
{expandedSections.sceneList ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>{t.sceneList}</span>
|
||||
<span>{t('buildSettings.sceneList')}</span>
|
||||
</div>
|
||||
{expandedSections.sceneList && (
|
||||
<div className="build-settings-field-content">
|
||||
@@ -583,7 +487,7 @@ export function BuildSettingsPanel({
|
||||
</div>
|
||||
<div className="build-settings-field-actions">
|
||||
<button className="build-settings-btn text" onClick={handleAddScene}>
|
||||
{t.addOpenScenes}
|
||||
{t('buildSettings.addOpenScenes')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,13 +501,13 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('scriptingDefines')}
|
||||
>
|
||||
{expandedSections.scriptingDefines ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>{t.scriptingDefines}</span>
|
||||
<span>{t('buildSettings.scriptingDefines')}</span>
|
||||
</div>
|
||||
{expandedSections.scriptingDefines && (
|
||||
<div className="build-settings-field-content">
|
||||
<div className="build-settings-defines-list">
|
||||
{settings.scriptingDefines.length === 0 ? (
|
||||
<div className="build-settings-empty-text">{t.listIsEmpty}</div>
|
||||
<div className="build-settings-empty-text">{t('buildSettings.listIsEmpty')}</div>
|
||||
) : (
|
||||
settings.scriptingDefines.map((define, index) => (
|
||||
<div key={index} className="build-settings-define-item">
|
||||
@@ -628,7 +532,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
{/* Platform Settings Section | 平台设置部分 */}
|
||||
<div className="build-settings-card">
|
||||
<div className="build-settings-card-header">{t.platformSettings}</div>
|
||||
<div className="build-settings-card-header">{t('buildSettings.platformSettings')}</div>
|
||||
|
||||
<div className="build-settings-field-group">
|
||||
<div
|
||||
@@ -636,13 +540,13 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('platformSettings')}
|
||||
>
|
||||
{expandedSections.platformSettings ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>{currentPlatformConfig?.label} Settings</span>
|
||||
<span>{currentPlatformConfig?.label} {t('buildSettings.settings')}</span>
|
||||
</div>
|
||||
{expandedSections.platformSettings && (
|
||||
<div className="build-settings-field-content">
|
||||
<div className="build-settings-form">
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.developmentBuild}</label>
|
||||
<label>{t('buildSettings.developmentBuild')}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.developmentBuild}
|
||||
@@ -653,7 +557,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.sourceMap}</label>
|
||||
<label>{t('buildSettings.sourceMap')}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.sourceMap}
|
||||
@@ -664,7 +568,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.compressionMethod}</label>
|
||||
<label>{t('buildSettings.compressionMethod')}</label>
|
||||
<select
|
||||
value={settings.compressionMethod}
|
||||
onChange={e => setSettings(prev => ({
|
||||
@@ -678,7 +582,7 @@ export function BuildSettingsPanel({
|
||||
</select>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.bundleModules}</label>
|
||||
<label>{t('buildSettings.bundleModules')}</label>
|
||||
<div className="build-settings-toggle-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -689,7 +593,7 @@ export function BuildSettingsPanel({
|
||||
}))}
|
||||
/>
|
||||
<span className="build-settings-hint">
|
||||
{settings.bundleModules ? t.bundleModulesHint : t.separateModulesHint}
|
||||
{settings.bundleModules ? t('buildSettings.bundleModulesHint') : t('buildSettings.separateModulesHint')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -702,7 +606,7 @@ export function BuildSettingsPanel({
|
||||
{/* Player Settings Overrides | 玩家设置覆盖 */}
|
||||
<div className="build-settings-card">
|
||||
<div className="build-settings-card-header">
|
||||
{t.playerSettingsOverrides}
|
||||
{t('buildSettings.playerSettingsOverrides')}
|
||||
<button className="build-settings-more-btn">
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
@@ -714,13 +618,13 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('playerSettings')}
|
||||
>
|
||||
{expandedSections.playerSettings ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>Player Settings</span>
|
||||
<span>{t('buildSettings.playerSettings')}</span>
|
||||
</div>
|
||||
{expandedSections.playerSettings && (
|
||||
<div className="build-settings-field-content">
|
||||
<div className="build-settings-form">
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.companyName}</label>
|
||||
<label>{t('buildSettings.companyName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.companyName}
|
||||
@@ -731,7 +635,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.productName}</label>
|
||||
<label>{t('buildSettings.productName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.productName}
|
||||
@@ -742,7 +646,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.version}</label>
|
||||
<label>{t('buildSettings.version')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.version}
|
||||
@@ -753,9 +657,9 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.defaultIcon}</label>
|
||||
<label>{t('buildSettings.defaultIcon')}</label>
|
||||
<div className="build-settings-icon-picker">
|
||||
<span>{t.none}</span>
|
||||
<span>{t('buildSettings.none')}</span>
|
||||
<span className="build-settings-icon-hint">(Texture 2D)</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -767,7 +671,7 @@ export function BuildSettingsPanel({
|
||||
</>
|
||||
) : (
|
||||
<div className="build-settings-no-selection">
|
||||
<p>Select a platform or build profile</p>
|
||||
<p>{t('buildSettings.selectPlatform')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -778,7 +682,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-progress-overlay">
|
||||
<div className="build-progress-dialog">
|
||||
<div className="build-progress-header">
|
||||
<h3>{t.buildInProgress}</h3>
|
||||
<h3>{t('buildSettings.buildInProgress')}</h3>
|
||||
{!isBuilding && (
|
||||
<button
|
||||
className="build-progress-close"
|
||||
@@ -806,9 +710,9 @@ export function BuildSettingsPanel({
|
||||
{isBuilding ? (
|
||||
buildProgress?.message || getStatusMessage(buildProgress?.status || BuildStatus.Preparing)
|
||||
) : buildResult?.success ? (
|
||||
t.buildSucceeded
|
||||
t('buildSettings.buildSucceeded')
|
||||
) : (
|
||||
t.buildFailed
|
||||
t('buildSettings.buildFailed')
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -831,11 +735,11 @@ export function BuildSettingsPanel({
|
||||
{buildResult.success && (
|
||||
<>
|
||||
<div className="build-result-row">
|
||||
<span className="build-result-label">{t.outputPath}:</span>
|
||||
<span className="build-result-label">{t('buildSettings.outputPath')}:</span>
|
||||
<span className="build-result-value">{buildResult.outputPath}</span>
|
||||
</div>
|
||||
<div className="build-result-row">
|
||||
<span className="build-result-label">{t.duration}:</span>
|
||||
<span className="build-result-label">{t('buildSettings.duration')}:</span>
|
||||
<span className="build-result-value">
|
||||
{(buildResult.duration / 1000).toFixed(2)}s
|
||||
</span>
|
||||
@@ -856,7 +760,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-result-warnings">
|
||||
<div className="build-result-warnings-header">
|
||||
<AlertTriangle size={14} />
|
||||
<span>{t.warnings} ({buildResult.warnings.length})</span>
|
||||
<span>{t('buildSettings.warnings')} ({buildResult.warnings.length})</span>
|
||||
</div>
|
||||
<ul className="build-result-warnings-list">
|
||||
{buildResult.warnings.map((warning, index) => (
|
||||
@@ -876,14 +780,14 @@ export function BuildSettingsPanel({
|
||||
className="build-settings-btn secondary"
|
||||
onClick={handleCancelBuild}
|
||||
>
|
||||
{t.cancel}
|
||||
{t('buildSettings.cancel')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="build-settings-btn primary"
|
||||
onClick={handleCloseBuildProgress}
|
||||
>
|
||||
{t.close}
|
||||
{t('buildSettings.close')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
import { X } from 'lucide-react';
|
||||
import type { BuildService, SceneManagerService } from '@esengine/editor-core';
|
||||
import { BuildSettingsPanel } from './BuildSettingsPanel';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/BuildSettingsWindow.css';
|
||||
|
||||
interface BuildSettingsWindowProps {
|
||||
projectPath?: string;
|
||||
locale?: string;
|
||||
buildService?: BuildService;
|
||||
sceneManager?: SceneManagerService;
|
||||
onClose: () => void;
|
||||
@@ -21,22 +21,17 @@ interface BuildSettingsWindowProps {
|
||||
|
||||
export function BuildSettingsWindow({
|
||||
projectPath,
|
||||
locale = 'en',
|
||||
buildService,
|
||||
sceneManager,
|
||||
onClose
|
||||
}: BuildSettingsWindowProps) {
|
||||
const t = locale === 'zh' ? {
|
||||
title: '构建设置'
|
||||
} : {
|
||||
title: 'Build Settings'
|
||||
};
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="build-settings-window-overlay">
|
||||
<div className="build-settings-window">
|
||||
<div className="build-settings-window-header">
|
||||
<h2>{t.title}</h2>
|
||||
<h2>{t('build.settingsTitle')}</h2>
|
||||
<button
|
||||
className="build-settings-window-close"
|
||||
onClick={onClose}
|
||||
@@ -48,7 +43,6 @@ export function BuildSettingsWindow({
|
||||
<div className="build-settings-window-content">
|
||||
<BuildSettingsPanel
|
||||
projectPath={projectPath}
|
||||
locale={locale}
|
||||
buildService={buildService}
|
||||
sceneManager={sceneManager}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Cpu } from 'lucide-react';
|
||||
import { ICompiler, CompileResult, CompilerContext } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/CompileDialog.css';
|
||||
|
||||
interface CompileDialogProps<TOptions = unknown> {
|
||||
@@ -18,6 +19,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
context,
|
||||
initialOptions
|
||||
}: CompileDialogProps<TOptions>) {
|
||||
const { t } = useLocale();
|
||||
const [options, setOptions] = useState<TOptions>(initialOptions as TOptions);
|
||||
const [isCompiling, setIsCompiling] = useState(false);
|
||||
const [result, setResult] = useState<CompileResult | null>(null);
|
||||
@@ -54,7 +56,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
} catch (error) {
|
||||
setResult({
|
||||
success: false,
|
||||
message: `编译失败: ${error}`,
|
||||
message: `${t('compileDialog.compileFailed')}: ${error}`,
|
||||
errors: [String(error)]
|
||||
});
|
||||
} finally {
|
||||
@@ -97,7 +99,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
</div>
|
||||
{result.outputFiles && result.outputFiles.length > 0 && (
|
||||
<div className="compile-dialog-output-files">
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>输出文件:</div>
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>{t('compileDialog.outputFiles')}:</div>
|
||||
{result.outputFiles.map((file, index) => (
|
||||
<div key={index} className="compile-dialog-output-file">
|
||||
{file}
|
||||
@@ -107,7 +109,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
)}
|
||||
{result.errors && result.errors.length > 0 && (
|
||||
<div className="compile-dialog-errors">
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>错误:</div>
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>{t('compileDialog.errors')}:</div>
|
||||
{result.errors.map((error, index) => (
|
||||
<div key={index} className="compile-dialog-error-item">
|
||||
{error}
|
||||
@@ -125,14 +127,14 @@ export function CompileDialog<TOptions = unknown>({
|
||||
className="compile-dialog-btn compile-dialog-btn-cancel"
|
||||
disabled={isCompiling}
|
||||
>
|
||||
关闭
|
||||
{t('compileDialog.close')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCompile}
|
||||
className="compile-dialog-btn compile-dialog-btn-primary"
|
||||
disabled={isCompiling || !!validationError}
|
||||
>
|
||||
{isCompiling ? '编译中...' : '编译'}
|
||||
{isCompiling ? t('compileDialog.compiling') : t('compileDialog.compile')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CompilerRegistry, ICompiler, CompilerContext, CompileResult, IFileSyste
|
||||
import { X, Play, Loader2 } from 'lucide-react';
|
||||
import { open as tauriOpen, save as tauriSave, message as tauriMessage, confirm as tauriConfirm } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/CompilerConfigDialog.css';
|
||||
|
||||
interface DirectoryEntry {
|
||||
@@ -29,6 +30,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
onClose,
|
||||
onCompileComplete
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [compiler, setCompiler] = useState<ICompiler | null>(null);
|
||||
const [options, setOptions] = useState<unknown>(null);
|
||||
const [isCompiling, setIsCompiling] = useState(false);
|
||||
@@ -164,7 +166,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
} catch (error) {
|
||||
setCompileResult({
|
||||
success: false,
|
||||
message: `编译失败: ${error}`,
|
||||
message: t('compilerConfig.compileFailed', { error: String(error) }),
|
||||
errors: [String(error)]
|
||||
});
|
||||
} finally {
|
||||
@@ -180,7 +182,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
<div className="compiler-dialog-overlay">
|
||||
<div className="compiler-dialog">
|
||||
<div className="compiler-dialog-header">
|
||||
<h3>{compiler?.name || '编译器配置'}</h3>
|
||||
<h3>{compiler?.name || t('compilerConfig.title')}</h3>
|
||||
<button className="close-button" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -191,7 +193,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
compiler.createConfigUI(handleOptionsChange, context)
|
||||
) : (
|
||||
<div className="no-config">
|
||||
{compiler ? '该编译器没有配置界面' : '编译器未找到'}
|
||||
{compiler ? t('compilerConfig.noConfigUI') : t('compilerConfig.compilerNotFound')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -201,7 +203,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
<div className="result-message">{compileResult.message}</div>
|
||||
{compileResult.outputFiles && compileResult.outputFiles.length > 0 && (
|
||||
<div className="output-files">
|
||||
已生成 {compileResult.outputFiles.length} 个文件
|
||||
{t('compilerConfig.generatedFiles', { count: compileResult.outputFiles.length })}
|
||||
</div>
|
||||
)}
|
||||
{compileResult.errors && compileResult.errors.length > 0 && (
|
||||
@@ -220,7 +222,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
onClick={onClose}
|
||||
disabled={isCompiling}
|
||||
>
|
||||
取消
|
||||
{t('compilerConfig.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="compile-button"
|
||||
@@ -230,12 +232,12 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
{isCompiling ? (
|
||||
<>
|
||||
<Loader2 size={16} className="spinning" />
|
||||
编译中...
|
||||
{t('compilerConfig.compiling')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={16} />
|
||||
编译
|
||||
{t('compilerConfig.compile')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import {
|
||||
Plus,
|
||||
Download,
|
||||
@@ -162,6 +163,7 @@ export function ContentBrowser({
|
||||
onDockInLayout,
|
||||
revealPath
|
||||
}: ContentBrowserProps) {
|
||||
const { t } = useLocale();
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const fileActionRegistry = Core.services.resolve(FileActionRegistry);
|
||||
|
||||
@@ -240,93 +242,6 @@ export function ContentBrowser({
|
||||
}
|
||||
}, [fileActionRegistry, messageHub]);
|
||||
|
||||
const t = {
|
||||
en: {
|
||||
favorites: 'Favorites',
|
||||
collections: 'Collections',
|
||||
add: 'Add',
|
||||
import: 'Import',
|
||||
saveAll: 'Save All',
|
||||
search: 'Search',
|
||||
items: 'items',
|
||||
dockInLayout: 'Dock in Layout',
|
||||
noProject: 'No project loaded',
|
||||
empty: 'This folder is empty',
|
||||
newFolder: 'New Folder',
|
||||
newPrefix: 'New',
|
||||
managedDirectoryTooltip: 'GUID-managed directory - Assets here get unique IDs for references',
|
||||
unmanagedWarning: 'This folder is not managed by GUID system. Assets created here cannot be referenced by GUID.',
|
||||
unmanagedWarningTitle: 'Unmanaged Directory',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
openInExplorer: 'Show in Explorer',
|
||||
copyPath: 'Copy Path',
|
||||
newSubfolder: 'New Subfolder',
|
||||
deleteConfirmTitle: 'Confirm Delete',
|
||||
deleteConfirmMessage: 'Are you sure you want to delete',
|
||||
cannotDeleteRoot: 'Cannot delete root directory'
|
||||
},
|
||||
zh: {
|
||||
favorites: '收藏夹',
|
||||
collections: '收藏集',
|
||||
add: '添加',
|
||||
import: '导入',
|
||||
saveAll: '全部保存',
|
||||
search: '搜索',
|
||||
items: '项',
|
||||
dockInLayout: '停靠到布局',
|
||||
noProject: '未加载项目',
|
||||
empty: '文件夹为空',
|
||||
newFolder: '新建文件夹',
|
||||
newPrefix: '新建',
|
||||
managedDirectoryTooltip: 'GUID 管理的目录 - 此处的资产会获得唯一 ID 以便引用',
|
||||
unmanagedWarning: '此文件夹不受 GUID 系统管理。在此创建的资产无法通过 GUID 引用。',
|
||||
unmanagedWarningTitle: '非托管目录',
|
||||
rename: '重命名',
|
||||
delete: '删除',
|
||||
openInExplorer: '在资源管理器中显示',
|
||||
copyPath: '复制路径',
|
||||
newSubfolder: '新建子文件夹',
|
||||
deleteConfirmTitle: '确认删除',
|
||||
deleteConfirmMessage: '确定要删除',
|
||||
cannotDeleteRoot: '无法删除根目录'
|
||||
}
|
||||
}[locale] || {
|
||||
favorites: 'Favorites',
|
||||
collections: 'Collections',
|
||||
add: 'Add',
|
||||
import: 'Import',
|
||||
saveAll: 'Save All',
|
||||
search: 'Search',
|
||||
items: 'items',
|
||||
dockInLayout: 'Dock in Layout',
|
||||
noProject: 'No project loaded',
|
||||
empty: 'This folder is empty',
|
||||
newFolder: 'New Folder',
|
||||
newPrefix: 'New',
|
||||
managedDirectoryTooltip: 'GUID-managed directory - Assets here get unique IDs for references',
|
||||
unmanagedWarning: 'This folder is not managed by GUID system. Assets created here cannot be referenced by GUID.',
|
||||
unmanagedWarningTitle: 'Unmanaged Directory',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
openInExplorer: 'Show in Explorer',
|
||||
copyPath: 'Copy Path',
|
||||
newSubfolder: 'New Subfolder',
|
||||
deleteConfirmTitle: 'Confirm Delete',
|
||||
deleteConfirmMessage: 'Are you sure you want to delete',
|
||||
cannotDeleteRoot: 'Cannot delete root directory'
|
||||
};
|
||||
|
||||
// 文件创建模板的 label 本地化映射
|
||||
const templateLabels: Record<string, { en: string; zh: string }> = {
|
||||
'Material': { en: 'Material', zh: '材质' },
|
||||
'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
|
||||
@@ -566,11 +481,20 @@ export class ${className} {
|
||||
}, [selectedPaths, assets, renameDialog, deleteConfirmDialog, createFileDialog]);
|
||||
|
||||
const getTemplateLabel = (label: string): string => {
|
||||
const mapping = templateLabels[label];
|
||||
if (mapping) {
|
||||
return locale === 'zh' ? mapping.zh : mapping.en;
|
||||
}
|
||||
return label;
|
||||
// Map template labels to translation keys
|
||||
const keyMap: Record<string, string> = {
|
||||
'Material': 'contentBrowser.templateLabels.material',
|
||||
'Shader': 'contentBrowser.templateLabels.shader',
|
||||
'Tilemap': 'contentBrowser.templateLabels.tilemap',
|
||||
'Tileset': 'contentBrowser.templateLabels.tileset',
|
||||
'Component': 'contentBrowser.templateLabels.component',
|
||||
'System': 'contentBrowser.templateLabels.system',
|
||||
'TypeScript': 'contentBrowser.templateLabels.typescript',
|
||||
'Inspector': 'contentBrowser.templateLabels.inspector',
|
||||
'Gizmo': 'contentBrowser.templateLabels.gizmo'
|
||||
};
|
||||
const key = keyMap[label];
|
||||
return key ? t(key) : label;
|
||||
};
|
||||
|
||||
// Build folder tree - use ref to avoid dependency cycle
|
||||
@@ -1106,7 +1030,7 @@ export class ${className} {
|
||||
// Show warning header if current path is not managed
|
||||
if (!isCurrentPathManaged && currentPath) {
|
||||
items.push({
|
||||
label: t.unmanagedWarningTitle,
|
||||
label: t('contentBrowser.unmanagedWarningTitle'),
|
||||
icon: <AlertTriangle size={16} className="warning-icon" />,
|
||||
disabled: true,
|
||||
onClick: () => {}
|
||||
@@ -1115,7 +1039,7 @@ export class ${className} {
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: t.newFolder,
|
||||
label: t('contentBrowser.newFolder'),
|
||||
icon: <FolderClosed size={16} />,
|
||||
onClick: async () => {
|
||||
if (!currentPath) return;
|
||||
@@ -1163,7 +1087,7 @@ export class ${className} {
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '在资源管理器中显示' : 'Show in Explorer',
|
||||
label: t('contentBrowser.openInExplorer'),
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
if (currentPath) {
|
||||
@@ -1178,7 +1102,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '刷新' : 'Refresh',
|
||||
label: t('contentBrowser.refresh'),
|
||||
icon: <RefreshCw size={16} />,
|
||||
onClick: async () => {
|
||||
if (currentPath) {
|
||||
@@ -1194,16 +1118,15 @@ export class ${className} {
|
||||
// Asset context menu
|
||||
if (asset.type === 'file') {
|
||||
items.push({
|
||||
label: locale === 'zh' ? '打开' : 'Open',
|
||||
label: t('contentBrowser.open'),
|
||||
icon: <File size={16} />,
|
||||
onClick: () => handleAssetDoubleClick(asset)
|
||||
});
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 保存
|
||||
items.push({
|
||||
label: locale === 'zh' ? '保存' : 'Save',
|
||||
label: t('contentBrowser.save'),
|
||||
icon: <Save size={16} />,
|
||||
shortcut: 'Ctrl+S',
|
||||
onClick: () => {
|
||||
@@ -1212,9 +1135,8 @@ export class ${className} {
|
||||
});
|
||||
}
|
||||
|
||||
// 重命名
|
||||
items.push({
|
||||
label: locale === 'zh' ? '重命名' : 'Rename',
|
||||
label: t('contentBrowser.rename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'F2',
|
||||
onClick: () => {
|
||||
@@ -1223,9 +1145,8 @@ export class ${className} {
|
||||
}
|
||||
});
|
||||
|
||||
// 批量重命名
|
||||
items.push({
|
||||
label: locale === 'zh' ? '批量重命名' : 'Batch Rename',
|
||||
label: t('contentBrowser.batchRename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'Shift+F2',
|
||||
disabled: true,
|
||||
@@ -1234,9 +1155,8 @@ export class ${className} {
|
||||
}
|
||||
});
|
||||
|
||||
// 复制
|
||||
items.push({
|
||||
label: locale === 'zh' ? '复制' : 'Duplicate',
|
||||
label: t('contentBrowser.duplicate'),
|
||||
icon: <Clipboard size={16} />,
|
||||
shortcut: 'Ctrl+D',
|
||||
onClick: () => {
|
||||
@@ -1244,9 +1164,8 @@ export class ${className} {
|
||||
}
|
||||
});
|
||||
|
||||
// 删除
|
||||
items.push({
|
||||
label: locale === 'zh' ? '删除' : 'Delete',
|
||||
label: t('contentBrowser.delete'),
|
||||
icon: <Trash2 size={16} />,
|
||||
shortcut: 'Delete',
|
||||
onClick: () => {
|
||||
@@ -1257,21 +1176,20 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 资产操作子菜单
|
||||
items.push({
|
||||
label: locale === 'zh' ? '资产操作' : 'Asset Actions',
|
||||
label: t('contentBrowser.assetActions'),
|
||||
icon: <Settings size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: locale === 'zh' ? '重新导入' : 'Reimport',
|
||||
label: t('contentBrowser.reimport'),
|
||||
icon: <RefreshCw size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Reimport asset:', asset.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: locale === 'zh' ? '导出...' : 'Export...',
|
||||
label: t('contentBrowser.export'),
|
||||
icon: <Package size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Export asset:', asset.path);
|
||||
@@ -1279,7 +1197,7 @@ export class ${className} {
|
||||
},
|
||||
{ label: '', separator: true, onClick: () => {} },
|
||||
{
|
||||
label: locale === 'zh' ? '迁移资产' : 'Migrate Asset',
|
||||
label: t('contentBrowser.migrateAsset'),
|
||||
icon: <Folder size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Migrate asset:', asset.path);
|
||||
@@ -1288,26 +1206,25 @@ export class ${className} {
|
||||
]
|
||||
});
|
||||
|
||||
// 资产本地化子菜单
|
||||
items.push({
|
||||
label: locale === 'zh' ? '资产本地化' : 'Asset Localization',
|
||||
label: t('contentBrowser.assetLocalization'),
|
||||
icon: <Globe size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: locale === 'zh' ? '创建本地化资产' : 'Create Localized Asset',
|
||||
label: t('contentBrowser.createLocalizedAsset'),
|
||||
onClick: () => {
|
||||
console.log('Create localized asset:', asset.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: locale === 'zh' ? '导入翻译' : 'Import Translation',
|
||||
label: t('contentBrowser.importTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Import translation:', asset.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: locale === 'zh' ? '导出翻译' : 'Export Translation',
|
||||
label: t('contentBrowser.exportTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Export translation:', asset.path);
|
||||
}
|
||||
@@ -1317,9 +1234,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 标签管理
|
||||
items.push({
|
||||
label: locale === 'zh' ? '管理标签' : 'Manage Tags',
|
||||
label: t('contentBrowser.manageTags'),
|
||||
icon: <Tag size={16} />,
|
||||
shortcut: 'Ctrl+T',
|
||||
onClick: () => {
|
||||
@@ -1329,9 +1245,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 路径复制选项
|
||||
items.push({
|
||||
label: locale === 'zh' ? '复制引用' : 'Copy Reference',
|
||||
label: t('contentBrowser.copyReference'),
|
||||
icon: <Link size={16} />,
|
||||
shortcut: 'Ctrl+C',
|
||||
onClick: () => {
|
||||
@@ -1340,7 +1255,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '拷贝Object路径' : 'Copy Object Path',
|
||||
label: t('contentBrowser.copyObjectPath'),
|
||||
icon: <Copy size={16} />,
|
||||
shortcut: 'Ctrl+Shift+C',
|
||||
onClick: () => {
|
||||
@@ -1350,7 +1265,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '拷贝包路径' : 'Copy Package Path',
|
||||
label: t('contentBrowser.copyPackagePath'),
|
||||
icon: <Package size={16} />,
|
||||
shortcut: 'Ctrl+Alt+C',
|
||||
onClick: () => {
|
||||
@@ -1361,9 +1276,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 引用查看器
|
||||
items.push({
|
||||
label: locale === 'zh' ? '引用查看器' : 'Reference Viewer',
|
||||
label: t('contentBrowser.referenceViewer'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+R',
|
||||
onClick: () => {
|
||||
@@ -1372,7 +1286,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '尺寸信息图' : 'Size Map',
|
||||
label: t('contentBrowser.sizeMap'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+D',
|
||||
onClick: () => {
|
||||
@@ -1382,9 +1296,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 在文件管理器中显示
|
||||
items.push({
|
||||
label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer',
|
||||
label: t('contentBrowser.openInExplorer'),
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1397,7 +1310,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [currentPath, fileCreationTemplates, handleAssetDoubleClick, loadAssets, locale, t.newFolder, t.newPrefix, t.unmanagedWarningTitle, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog, projectPath]);
|
||||
}, [currentPath, fileCreationTemplates, handleAssetDoubleClick, loadAssets, t, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog, projectPath, getTemplateLabel]);
|
||||
|
||||
/**
|
||||
* Handle folder tree context menu
|
||||
@@ -1414,7 +1327,7 @@ export class ${className} {
|
||||
|
||||
// New subfolder
|
||||
items.push({
|
||||
label: t.newSubfolder,
|
||||
label: t('contentBrowser.newSubfolder'),
|
||||
icon: <FolderClosed size={16} />,
|
||||
onClick: async () => {
|
||||
const folderPath = `${node.path}/New Folder`;
|
||||
@@ -1434,7 +1347,7 @@ export class ${className} {
|
||||
// Rename (not for root)
|
||||
if (!isRoot) {
|
||||
items.push({
|
||||
label: t.rename,
|
||||
label: t('contentBrowser.rename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
onClick: () => {
|
||||
setRenameDialog({
|
||||
@@ -1452,7 +1365,7 @@ export class ${className} {
|
||||
// Delete (not for root)
|
||||
if (!isRoot) {
|
||||
items.push({
|
||||
label: t.delete,
|
||||
label: t('contentBrowser.delete'),
|
||||
icon: <Trash2 size={16} />,
|
||||
onClick: () => {
|
||||
setDeleteConfirmDialog({
|
||||
@@ -1464,7 +1377,7 @@ export class ${className} {
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: t.cannotDeleteRoot,
|
||||
label: t('contentBrowser.cannotDeleteRoot'),
|
||||
icon: <Trash2 size={16} />,
|
||||
disabled: true,
|
||||
onClick: () => {}
|
||||
@@ -1475,7 +1388,7 @@ export class ${className} {
|
||||
|
||||
// Copy path
|
||||
items.push({
|
||||
label: t.copyPath,
|
||||
label: t('contentBrowser.copyPath'),
|
||||
icon: <Clipboard size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1488,7 +1401,7 @@ export class ${className} {
|
||||
|
||||
// Show in explorer
|
||||
items.push({
|
||||
label: t.openInExplorer,
|
||||
label: t('contentBrowser.openInExplorer'),
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1503,7 +1416,7 @@ export class ${className} {
|
||||
position: { x: e.clientX, y: e.clientY },
|
||||
items
|
||||
});
|
||||
}, [projectPath, t, refreshAll, setRenameDialog, setDeleteConfirmDialog, setFolderTreeContextMenu, setExpandedFolders]);
|
||||
}, [projectPath, t, refreshAll, setRenameDialog, setDeleteConfirmDialog, setFolderTreeContextMenu, setExpandedFolders, getTemplateLabel]);
|
||||
|
||||
// Render folder tree node
|
||||
const renderFolderNode = useCallback((node: FolderNode, depth: number = 0) => {
|
||||
@@ -1521,7 +1434,7 @@ export class ${className} {
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => handleFolderSelect(node.path)}
|
||||
onContextMenu={(e) => handleFolderTreeContextMenu(e, node)}
|
||||
title={isRootManaged ? t.managedDirectoryTooltip : undefined}
|
||||
title={isRootManaged ? t('contentBrowser.managedDirectoryTooltip') : undefined}
|
||||
onDragOver={(e) => handleFolderDragOver(e, node.path)}
|
||||
onDragLeave={handleFolderDragLeave}
|
||||
onDrop={(e) => handleFolderDrop(e, node.path)}
|
||||
@@ -1545,13 +1458,13 @@ export class ${className} {
|
||||
</span>
|
||||
<span className="folder-tree-name">{node.name}</span>
|
||||
{isRootManaged && (
|
||||
<span className="managed-badge" title={t.managedDirectoryTooltip}>GUID</span>
|
||||
<span className="managed-badge" title={t('contentBrowser.managedDirectoryTooltip')}>GUID</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
</div>
|
||||
);
|
||||
}, [currentPath, expandedFolders, handleFolderSelect, handleFolderTreeContextMenu, toggleFolderExpand, projectPath, t.managedDirectoryTooltip, dragOverFolder, handleFolderDragOver, handleFolderDragLeave, handleFolderDrop]);
|
||||
}, [currentPath, expandedFolders, handleFolderSelect, handleFolderTreeContextMenu, toggleFolderExpand, projectPath, t, dragOverFolder, handleFolderDragOver, handleFolderDragLeave, handleFolderDrop]);
|
||||
|
||||
// Filter assets by search
|
||||
const filteredAssets = searchQuery.trim()
|
||||
@@ -1564,7 +1477,7 @@ export class ${className} {
|
||||
return (
|
||||
<div className="content-browser">
|
||||
<div className="content-browser-empty">
|
||||
<p>{t.noProject}</p>
|
||||
<p>{t('contentBrowser.noProject')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1585,7 +1498,7 @@ export class ${className} {
|
||||
onClick={() => setFavoritesExpanded(!favoritesExpanded)}
|
||||
>
|
||||
{favoritesExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span>{t.favorites}</span>
|
||||
<span>{t('contentBrowser.favorites')}</span>
|
||||
<button className="cb-section-btn" onClick={(e) => e.stopPropagation()}>
|
||||
<Search size={12} />
|
||||
</button>
|
||||
@@ -1620,7 +1533,7 @@ export class ${className} {
|
||||
onClick={() => setCollectionsExpanded(!collectionsExpanded)}
|
||||
>
|
||||
{collectionsExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span>{t.collections}</span>
|
||||
<span>{t('contentBrowser.collections')}</span>
|
||||
<div className="cb-section-actions">
|
||||
<button className="cb-section-btn" onClick={(e) => e.stopPropagation()}>
|
||||
<Plus size={12} />
|
||||
@@ -1645,15 +1558,15 @@ export class ${className} {
|
||||
<div className="cb-toolbar-left">
|
||||
<button className="cb-toolbar-btn primary">
|
||||
<Plus size={14} />
|
||||
<span>{t.add}</span>
|
||||
<span>{t('contentBrowser.add')}</span>
|
||||
</button>
|
||||
<button className="cb-toolbar-btn">
|
||||
<Download size={14} />
|
||||
<span>{t.import}</span>
|
||||
<span>{t('contentBrowser.import')}</span>
|
||||
</button>
|
||||
<button className="cb-toolbar-btn">
|
||||
<Save size={14} />
|
||||
<span>{t.saveAll}</span>
|
||||
<span>{t('contentBrowser.saveAll')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1677,10 +1590,10 @@ export class ${className} {
|
||||
<button
|
||||
className="cb-toolbar-btn dock-btn"
|
||||
onClick={onDockInLayout}
|
||||
title={t.dockInLayout}
|
||||
title={t('contentBrowser.dockInLayout')}
|
||||
>
|
||||
<PanelRightClose size={14} />
|
||||
<span>{t.dockInLayout}</span>
|
||||
<span>{t('contentBrowser.dockInLayout')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1697,7 +1610,7 @@ export class ${className} {
|
||||
<input
|
||||
type="text"
|
||||
className="cb-search-input"
|
||||
placeholder={`${t.search} ${breadcrumbs[breadcrumbs.length - 1]?.name || ''}`}
|
||||
placeholder={`${t('contentBrowser.search')} ${breadcrumbs[breadcrumbs.length - 1]?.name || ''}`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -1726,7 +1639,7 @@ export class ${className} {
|
||||
{loading ? (
|
||||
<div className="cb-loading">Loading...</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="cb-empty">{t.empty}</div>
|
||||
<div className="cb-empty">{t('contentBrowser.empty')}</div>
|
||||
) : (
|
||||
filteredAssets.map(asset => {
|
||||
const isDragOverAsset = asset.type === 'folder' && dragOverFolder === asset.path;
|
||||
@@ -1793,7 +1706,7 @@ export class ${className} {
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="cb-status-bar">
|
||||
<span>{filteredAssets.length} {t.items}</span>
|
||||
<span>{filteredAssets.length} {t('contentBrowser.items')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1820,7 +1733,7 @@ export class ${className} {
|
||||
<div className="cb-dialog-overlay" onClick={() => setRenameDialog(null)}>
|
||||
<div className="cb-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="cb-dialog-header">
|
||||
<h3>{locale === 'zh' ? '重命名' : 'Rename'}</h3>
|
||||
<h3>{t('contentBrowser.dialogs.renameTitle')}</h3>
|
||||
</div>
|
||||
<div className="cb-dialog-body">
|
||||
<input
|
||||
@@ -1836,13 +1749,13 @@ export class ${className} {
|
||||
</div>
|
||||
<div className="cb-dialog-footer">
|
||||
<button className="cb-btn" onClick={() => setRenameDialog(null)}>
|
||||
{locale === 'zh' ? '取消' : 'Cancel'}
|
||||
{t('contentBrowser.dialogs.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="cb-btn primary"
|
||||
onClick={() => handleRename(renameDialog.asset, renameDialog.newName)}
|
||||
>
|
||||
{locale === 'zh' ? '确定' : 'OK'}
|
||||
{t('contentBrowser.dialogs.ok')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1854,24 +1767,22 @@ export class ${className} {
|
||||
<div className="cb-dialog-overlay" onClick={() => setDeleteConfirmDialog(null)}>
|
||||
<div className="cb-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="cb-dialog-header">
|
||||
<h3>{locale === 'zh' ? '确认删除' : 'Confirm Delete'}</h3>
|
||||
<h3>{t('contentBrowser.deleteConfirmTitle')}</h3>
|
||||
</div>
|
||||
<div className="cb-dialog-body">
|
||||
<p>
|
||||
{locale === 'zh'
|
||||
? `确定要删除 "${deleteConfirmDialog.name}" 吗?`
|
||||
: `Delete "${deleteConfirmDialog.name}"?`}
|
||||
{t('contentBrowser.deleteConfirmMessage', { name: deleteConfirmDialog.name })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="cb-dialog-footer">
|
||||
<button className="cb-btn" onClick={() => setDeleteConfirmDialog(null)}>
|
||||
{locale === 'zh' ? '取消' : 'Cancel'}
|
||||
{t('contentBrowser.dialogs.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="cb-btn danger"
|
||||
onClick={() => handleDelete(deleteConfirmDialog)}
|
||||
>
|
||||
{locale === 'zh' ? '删除' : 'Delete'}
|
||||
{t('contentBrowser.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1887,11 +1798,11 @@ export class ${className} {
|
||||
: `.${createFileDialog.template.extension}`;
|
||||
return (
|
||||
<PromptDialog
|
||||
title={locale === 'zh' ? `新建 ${getTemplateLabel(createFileDialog.template.label)}` : `New ${createFileDialog.template.label}`}
|
||||
message={locale === 'zh' ? `输入文件名(将添加 ${ext}):` : `Enter file name (${ext} will be added):`}
|
||||
title={t('contentBrowser.dialogs.newFile', { type: getTemplateLabel(createFileDialog.template.label) })}
|
||||
message={t('contentBrowser.dialogs.enterFileName', { ext })}
|
||||
placeholder="filename"
|
||||
confirmText={locale === 'zh' ? '创建' : 'Create'}
|
||||
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
|
||||
confirmText={t('contentBrowser.dialogs.create')}
|
||||
cancelText={t('contentBrowser.dialogs.cancel')}
|
||||
onConfirm={async (value) => {
|
||||
const { parentPath, template } = createFileDialog;
|
||||
setCreateFileDialog(null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, File, FolderTree, FolderOpen } from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/ExportRuntimeDialog.css';
|
||||
|
||||
interface ExportRuntimeDialogProps {
|
||||
@@ -33,6 +34,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
currentFileName,
|
||||
projectPath
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [selectedMode, setSelectedMode] = useState<'single' | 'workspace'>('workspace');
|
||||
const [assetOutputPath, setAssetOutputPath] = useState('');
|
||||
const [typeOutputPath, setTypeOutputPath] = useState('');
|
||||
@@ -116,7 +118,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: '选择资产输出目录',
|
||||
title: t('exportRuntime.selectAssetDir'),
|
||||
defaultPath: assetOutputPath || projectPath
|
||||
});
|
||||
if (selected) {
|
||||
@@ -134,7 +136,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: '选择类型定义输出目录',
|
||||
title: t('exportRuntime.selectTypeDir'),
|
||||
defaultPath: typeOutputPath || projectPath
|
||||
});
|
||||
if (selected) {
|
||||
@@ -149,22 +151,22 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!assetOutputPath) {
|
||||
setExportMessage('错误:请选择资产输出路径');
|
||||
setExportMessage(t('exportRuntime.errorSelectAssetPath'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!typeOutputPath) {
|
||||
setExportMessage('错误:请选择类型定义输出路径');
|
||||
setExportMessage(t('exportRuntime.errorSelectTypePath'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMode === 'workspace' && selectedFiles.size === 0) {
|
||||
setExportMessage('错误:请至少选择一个文件');
|
||||
setExportMessage(t('exportRuntime.errorSelectFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMode === 'single' && !currentFileName) {
|
||||
setExportMessage('错误:没有可导出的当前文件');
|
||||
setExportMessage(t('exportRuntime.errorNoCurrentFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
|
||||
setIsExporting(true);
|
||||
setExportProgress(0);
|
||||
setExportMessage('正在导出...');
|
||||
setExportMessage(t('exportRuntime.exporting'));
|
||||
|
||||
try {
|
||||
await onExport({
|
||||
@@ -186,9 +188,9 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
});
|
||||
|
||||
setExportProgress(100);
|
||||
setExportMessage('导出成功!');
|
||||
setExportMessage(t('exportRuntime.exportSuccess'));
|
||||
} catch (error) {
|
||||
setExportMessage(`导出失败:${error}`);
|
||||
setExportMessage(t('exportRuntime.exportFailed', { error: String(error) }));
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -198,7 +200,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
<div className="export-dialog-overlay">
|
||||
<div className="export-dialog" style={{ maxWidth: '700px', width: '90%' }}>
|
||||
<div className="export-dialog-header">
|
||||
<h3>导出运行时资产</h3>
|
||||
<h3>{t('exportRuntime.title')}</h3>
|
||||
<button onClick={onClose} className="export-dialog-close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -213,26 +215,26 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
disabled={!hasProject}
|
||||
>
|
||||
<FolderTree size={16} />
|
||||
工作区导出
|
||||
{t('exportRuntime.workspaceExport')}
|
||||
</button>
|
||||
<button
|
||||
className={`export-mode-tab ${selectedMode === 'single' ? 'active' : ''}`}
|
||||
onClick={() => setSelectedMode('single')}
|
||||
>
|
||||
<File size={16} />
|
||||
当前文件
|
||||
{t('exportRuntime.currentFile')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 资产输出路径 */}
|
||||
<div className="export-section">
|
||||
<h4>资产输出路径</h4>
|
||||
<h4>{t('exportRuntime.assetOutputPath')}</h4>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={assetOutputPath}
|
||||
onChange={(e) => setAssetOutputPath(e.target.value)}
|
||||
placeholder="选择资产输出目录(.btree.bin / .btree.json)..."
|
||||
placeholder={t('exportRuntime.selectAssetDirPlaceholder')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
@@ -259,31 +261,26 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
浏览
|
||||
{t('exportRuntime.browse')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TypeScript 类型定义输出路径 */}
|
||||
<div className="export-section">
|
||||
<h4>TypeScript 类型定义输出路径</h4>
|
||||
<div style={{ marginBottom: '12px', fontSize: '11px', color: '#999', lineHeight: '1.5' }}>
|
||||
{selectedMode === 'workspace' ? (
|
||||
<>
|
||||
将导出以下类型定义:<br />
|
||||
• 每个行为树的黑板变量类型(.d.ts)<br />
|
||||
• 全局黑板变量类型(GlobalBlackboard.ts)
|
||||
</>
|
||||
) : (
|
||||
'将导出当前行为树的黑板变量类型(.d.ts)'
|
||||
)}
|
||||
<h4>{t('exportRuntime.typeOutputPath')}</h4>
|
||||
<div style={{ marginBottom: '12px', fontSize: '11px', color: '#999', lineHeight: '1.5', whiteSpace: 'pre-line' }}>
|
||||
{selectedMode === 'workspace'
|
||||
? t('exportRuntime.typeOutputHintWorkspace')
|
||||
: t('exportRuntime.typeOutputHintSingle')
|
||||
}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={typeOutputPath}
|
||||
onChange={(e) => setTypeOutputPath(e.target.value)}
|
||||
placeholder="选择类型定义输出目录..."
|
||||
placeholder={t('exportRuntime.selectTypeDirPlaceholder')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
@@ -310,7 +307,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
浏览
|
||||
{t('exportRuntime.browse')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,7 +317,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
<div className="export-section">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<h4 style={{ margin: 0, fontSize: '13px', color: '#ccc' }}>
|
||||
选择要导出的文件 ({selectedFiles.size}/{availableFiles.length})
|
||||
{t('exportRuntime.selectFilesToExport')} ({selectedFiles.size}/{availableFiles.length})
|
||||
</h4>
|
||||
<button
|
||||
onClick={handleSelectAll}
|
||||
@@ -334,7 +331,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{selectAll ? '取消全选' : '全选'}
|
||||
{selectAll ? t('exportRuntime.deselectAll') : t('exportRuntime.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="export-file-list">
|
||||
@@ -359,8 +356,8 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
onChange={(e) => handleFileFormatChange(file, e.target.value as 'json' | 'binary')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<option value="binary">二进制</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="binary">{t('exportRuntime.binary')}</option>
|
||||
<option value="json">{t('exportRuntime.json')}</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
@@ -371,7 +368,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
{/* 单文件模式 */}
|
||||
{selectedMode === 'single' && (
|
||||
<div className="export-section">
|
||||
<h4>当前文件</h4>
|
||||
<h4>{t('exportRuntime.currentFile')}</h4>
|
||||
{currentFileName ? (
|
||||
<div className="export-file-list">
|
||||
<div className="export-file-item selected">
|
||||
@@ -384,8 +381,8 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
value={fileFormats.get(currentFileName) || 'binary'}
|
||||
onChange={(e) => handleFileFormatChange(currentFileName, e.target.value as 'json' | 'binary')}
|
||||
>
|
||||
<option value="binary">二进制</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="binary">{t('exportRuntime.binary')}</option>
|
||||
<option value="json">{t('exportRuntime.json')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,9 +397,9 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
border: '1px solid #3a3a3a'
|
||||
}}>
|
||||
<File size={32} style={{ margin: '0 auto 12px', opacity: 0.5 }} />
|
||||
<div>没有打开的行为树文件</div>
|
||||
<div>{t('exportRuntime.noOpenFile')}</div>
|
||||
<div style={{ fontSize: '11px', marginTop: '8px' }}>
|
||||
请先在编辑器中打开一个行为树文件
|
||||
{t('exportRuntime.openFileHint')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -415,7 +412,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
<div style={{
|
||||
flex: 1,
|
||||
fontSize: '12px',
|
||||
color: exportMessage.startsWith('错误') ? '#f48771' : exportMessage.includes('成功') ? '#89d185' : '#ccc',
|
||||
color: exportMessage.includes('Error') || exportMessage.includes('错误') ? '#f48771' : exportMessage.includes('success') || exportMessage.includes('成功') ? '#89d185' : '#ccc',
|
||||
paddingLeft: '8px'
|
||||
}}>
|
||||
{exportMessage}
|
||||
@@ -439,7 +436,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<button onClick={onClose} className="export-dialog-btn export-dialog-btn-cancel">
|
||||
关闭
|
||||
{t('exportRuntime.close')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
@@ -447,7 +444,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
disabled={isExporting}
|
||||
style={{ opacity: isExporting ? 0.5 : 1 }}
|
||||
>
|
||||
{isExporting ? '导出中...' : '导出'}
|
||||
{isExporting ? t('exportRuntime.exporting') : t('exportRuntime.export')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Core } from '@esengine/ecs-framework';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import { PromptDialog } from './PromptDialog';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/FileTree.css';
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export interface FileTreeHandle {
|
||||
}
|
||||
|
||||
export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, onSelectFile, onSelectFiles, selectedPath, selectedPaths, messageHub, searchQuery, showFiles = true, onOpenScene }, ref) => {
|
||||
const { t } = useLocale();
|
||||
const [tree, setTree] = useState<TreeNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [internalSelectedPath, setInternalSelectedPath] = useState<string | null>(null);
|
||||
@@ -475,7 +477,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
setNewName('');
|
||||
} catch (error) {
|
||||
console.error('Failed to rename:', error);
|
||||
alert(`重命名失败: ${error}`);
|
||||
alert(`${t('fileTree.renameFailed')}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -499,7 +501,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
await refreshTree();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete:', error);
|
||||
alert(`删除失败: ${error}`);
|
||||
alert(`${t('fileTree.deleteFailed')}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -549,7 +551,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
await refreshTree();
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${type}:`, error);
|
||||
alert(`${type === 'create-file' ? '创建文件' : type === 'create-folder' ? '创建文件夹' : '创建模板文件'}失败: ${error}`);
|
||||
const errorKey = type === 'create-file' ? 'fileTree.createFileFailed' :
|
||||
type === 'create-folder' ? 'fileTree.createFolderFailed' : 'fileTree.createTemplateFailed';
|
||||
alert(`${t(errorKey)}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -557,12 +561,12 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
if (!node) {
|
||||
const baseItems: ContextMenuItem[] = [
|
||||
{
|
||||
label: '新建文件',
|
||||
label: t('fileTree.newFile'),
|
||||
icon: <FileText size={16} />,
|
||||
onClick: () => rootPath && handleCreateFileClick(rootPath)
|
||||
},
|
||||
{
|
||||
label: '新建文件夹',
|
||||
label: t('fileTree.newFolder'),
|
||||
icon: <FolderPlus size={16} />,
|
||||
onClick: () => rootPath && handleCreateFolderClick(rootPath)
|
||||
}
|
||||
@@ -589,7 +593,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
if (node.type === 'file') {
|
||||
items.push({
|
||||
label: '打开文件',
|
||||
label: t('fileTree.openFile'),
|
||||
icon: <File size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -621,9 +625,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 文件操作菜单项
|
||||
// 文件操作菜单项 | File operation menu items
|
||||
items.push({
|
||||
label: '保存',
|
||||
label: t('fileTree.save'),
|
||||
icon: <Save size={16} />,
|
||||
shortcut: 'Ctrl+S',
|
||||
onClick: () => {
|
||||
@@ -634,7 +638,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: '重命名',
|
||||
label: t('fileTree.rename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'F2',
|
||||
onClick: () => {
|
||||
@@ -644,7 +648,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '批量重命名',
|
||||
label: t('fileTree.batchRename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'Shift+F2',
|
||||
disabled: true, // TODO: 实现批量重命名
|
||||
@@ -654,7 +658,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '复制',
|
||||
label: t('fileTree.duplicate'),
|
||||
icon: <Clipboard size={16} />,
|
||||
shortcut: 'Ctrl+D',
|
||||
onClick: () => {
|
||||
@@ -664,7 +668,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '删除',
|
||||
label: t('fileTree.delete'),
|
||||
icon: <Trash2 size={16} />,
|
||||
shortcut: 'Delete',
|
||||
onClick: () => handleDeleteClick(node)
|
||||
@@ -672,21 +676,21 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 资产操作子菜单
|
||||
// 资产操作子菜单 | Asset operations submenu
|
||||
items.push({
|
||||
label: '资产操作',
|
||||
label: t('fileTree.assetActions'),
|
||||
icon: <Settings size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: '重新导入',
|
||||
label: t('fileTree.reimport'),
|
||||
icon: <RefreshCw size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Reimport asset:', node.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '导出...',
|
||||
label: t('fileTree.exportAsset'),
|
||||
icon: <Package size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Export asset:', node.path);
|
||||
@@ -694,7 +698,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
},
|
||||
{ label: '', separator: true, onClick: () => {} },
|
||||
{
|
||||
label: '迁移资产',
|
||||
label: t('fileTree.migrateAsset'),
|
||||
icon: <Folder size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Migrate asset:', node.path);
|
||||
@@ -703,26 +707,26 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
]
|
||||
});
|
||||
|
||||
// 资产本地化子菜单
|
||||
// 资产本地化子菜单 | Asset localization submenu
|
||||
items.push({
|
||||
label: '资产本地化',
|
||||
label: t('fileTree.assetLocalization'),
|
||||
icon: <Globe size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: '创建本地化资产',
|
||||
label: t('fileTree.createLocalizedAsset'),
|
||||
onClick: () => {
|
||||
console.log('Create localized asset:', node.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '导入翻译',
|
||||
label: t('fileTree.importTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Import translation:', node.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '导出翻译',
|
||||
label: t('fileTree.exportTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Export translation:', node.path);
|
||||
}
|
||||
@@ -732,9 +736,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 标签和引用
|
||||
// 标签和引用 | Tags and references
|
||||
items.push({
|
||||
label: '管理标签',
|
||||
label: t('fileTree.manageTags'),
|
||||
icon: <Tag size={16} />,
|
||||
shortcut: 'Ctrl+T',
|
||||
onClick: () => {
|
||||
@@ -744,9 +748,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 路径复制选项
|
||||
// 路径复制选项 | Path copy options
|
||||
items.push({
|
||||
label: '复制引用',
|
||||
label: t('fileTree.copyReference'),
|
||||
icon: <Link size={16} />,
|
||||
shortcut: 'Ctrl+C',
|
||||
onClick: () => {
|
||||
@@ -755,22 +759,22 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '拷贝Object路径',
|
||||
label: t('fileTree.copyObjectPath'),
|
||||
icon: <Copy size={16} />,
|
||||
shortcut: 'Ctrl+Shift+C',
|
||||
onClick: () => {
|
||||
// 生成对象路径格式
|
||||
// 生成对象路径格式 | Generate object path format
|
||||
const objectPath = node.path.replace(/\\/g, '/');
|
||||
navigator.clipboard.writeText(objectPath);
|
||||
}
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '拷贝包路径',
|
||||
label: t('fileTree.copyPackagePath'),
|
||||
icon: <Package size={16} />,
|
||||
shortcut: 'Ctrl+Alt+C',
|
||||
onClick: () => {
|
||||
// 生成包路径格式
|
||||
// 生成包路径格式 | Generate package path format
|
||||
const packagePath = '/' + node.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
|
||||
navigator.clipboard.writeText(packagePath);
|
||||
}
|
||||
@@ -778,9 +782,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 引用查看器
|
||||
// 引用查看器 | Reference viewer
|
||||
items.push({
|
||||
label: '引用查看器',
|
||||
label: t('fileTree.referenceViewer'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+R',
|
||||
onClick: () => {
|
||||
@@ -789,7 +793,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '尺寸信息图',
|
||||
label: t('fileTree.sizeMap'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+D',
|
||||
onClick: () => {
|
||||
@@ -801,13 +805,13 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
if (node.type === 'folder') {
|
||||
items.push({
|
||||
label: '新建文件',
|
||||
label: t('fileTree.newFile'),
|
||||
icon: <FileText size={16} />,
|
||||
onClick: () => handleCreateFileClick(node.path)
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '新建文件夹',
|
||||
label: t('fileTree.newFolder'),
|
||||
icon: <FolderPlus size={16} />,
|
||||
onClick: () => handleCreateFolderClick(node.path)
|
||||
});
|
||||
@@ -830,7 +834,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: '在文件管理器中显示',
|
||||
label: t('fileTree.showInExplorer'),
|
||||
icon: <FolderOpen size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1070,11 +1074,11 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="file-tree loading">Loading...</div>;
|
||||
return <div className="file-tree loading">{t('fileTree.loading')}</div>;
|
||||
}
|
||||
|
||||
if (!rootPath || tree.length === 0) {
|
||||
return <div className="file-tree empty">No folders</div>;
|
||||
return <div className="file-tree empty">{t('fileTree.noFolders')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1099,14 +1103,14 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
)}
|
||||
{deleteDialog && (
|
||||
<ConfirmDialog
|
||||
title="确认删除"
|
||||
title={t('fileTree.confirmDelete')}
|
||||
message={
|
||||
deleteDialog.node.type === 'folder'
|
||||
? `确定要删除文件夹 "${deleteDialog.node.name}" 及其所有内容吗?\n此操作无法撤销。`
|
||||
: `确定要删除文件 "${deleteDialog.node.name}" 吗?\n此操作无法撤销。`
|
||||
? t('fileTree.confirmDeleteFolder', { name: deleteDialog.node.name })
|
||||
: t('fileTree.confirmDeleteFile', { name: deleteDialog.node.name })
|
||||
}
|
||||
confirmText="删除"
|
||||
cancelText="取消"
|
||||
confirmText={t('fileTree.delete')}
|
||||
cancelText={t('fileTree.cancel')}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => setDeleteDialog(null)}
|
||||
/>
|
||||
@@ -1114,22 +1118,22 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
{promptDialog && (
|
||||
<PromptDialog
|
||||
title={
|
||||
promptDialog.type === 'create-file' ? '新建文件' :
|
||||
promptDialog.type === 'create-folder' ? '新建文件夹' :
|
||||
'新建文件'
|
||||
promptDialog.type === 'create-file' ? t('fileTree.newFileTitle') :
|
||||
promptDialog.type === 'create-folder' ? t('fileTree.newFolderTitle') :
|
||||
t('fileTree.newFileTitle')
|
||||
}
|
||||
message={
|
||||
promptDialog.type === 'create-file' ? '请输入文件名:' :
|
||||
promptDialog.type === 'create-folder' ? '请输入文件夹名:' :
|
||||
`请输入文件名 (将自动添加 .${promptDialog.templateExtension} 扩展名):`
|
||||
promptDialog.type === 'create-file' ? t('fileTree.enterFileName') :
|
||||
promptDialog.type === 'create-folder' ? t('fileTree.enterFolderName') :
|
||||
t('fileTree.enterTemplateFileName', { ext: promptDialog.templateExtension || '' })
|
||||
}
|
||||
placeholder={
|
||||
promptDialog.type === 'create-file' ? '例如: config.json' :
|
||||
promptDialog.type === 'create-folder' ? '例如: assets' :
|
||||
'例如: MyFile'
|
||||
promptDialog.type === 'create-file' ? t('fileTree.fileNamePlaceholder') :
|
||||
promptDialog.type === 'create-folder' ? t('fileTree.folderNamePlaceholder') :
|
||||
t('fileTree.templateNamePlaceholder')
|
||||
}
|
||||
confirmText="创建"
|
||||
cancelText="取消"
|
||||
confirmText={t('fileTree.create')}
|
||||
cancelText={t('fileTree.cancel')}
|
||||
onConfirm={handlePromptConfirm}
|
||||
onCancel={() => setPromptDialog(null)}
|
||||
/>
|
||||
|
||||
@@ -2,15 +2,16 @@ import { useState } from 'react';
|
||||
import { Github, AlertCircle, CheckCircle, Loader, ExternalLink } from 'lucide-react';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/GitHubAuth.css';
|
||||
|
||||
interface GitHubAuthProps {
|
||||
githubService: GitHubService;
|
||||
onSuccess: () => void;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps) {
|
||||
export function GitHubAuth({ githubService, onSuccess }: GitHubAuthProps) {
|
||||
const { t } = useLocale();
|
||||
const [useOAuth, setUseOAuth] = useState(true);
|
||||
const [githubToken, setGithubToken] = useState('');
|
||||
const [userCode, setUserCode] = useState('');
|
||||
@@ -18,54 +19,6 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
const [authStatus, setAuthStatus] = useState<'idle' | 'pending' | 'authorized' | 'error'>('idle');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
githubLogin: 'GitHub 登录',
|
||||
oauthLogin: 'OAuth 登录(推荐)',
|
||||
tokenLogin: 'Token 登录',
|
||||
oauthStep1: '1. 点击"开始授权"按钮',
|
||||
oauthStep2: '2. 在浏览器中打开 GitHub 授权页面',
|
||||
oauthStep3: '3. 输入下方显示的代码并授权',
|
||||
startAuth: '开始授权',
|
||||
authorizing: '等待授权中...',
|
||||
authorized: '授权成功!',
|
||||
authFailed: '授权失败',
|
||||
userCode: '授权码',
|
||||
copyCode: '复制代码',
|
||||
openBrowser: '打开浏览器',
|
||||
tokenLabel: 'GitHub Personal Access Token',
|
||||
tokenPlaceholder: '粘贴你的 GitHub Token',
|
||||
tokenHint: '需要 repo 和 workflow 权限',
|
||||
createToken: '创建 Token',
|
||||
login: '登录',
|
||||
back: '返回'
|
||||
},
|
||||
en: {
|
||||
githubLogin: 'GitHub Login',
|
||||
oauthLogin: 'OAuth Login (Recommended)',
|
||||
tokenLogin: 'Token Login',
|
||||
oauthStep1: '1. Click "Start Authorization"',
|
||||
oauthStep2: '2. Open GitHub authorization page in browser',
|
||||
oauthStep3: '3. Enter the code shown below and authorize',
|
||||
startAuth: 'Start Authorization',
|
||||
authorizing: 'Waiting for authorization...',
|
||||
authorized: 'Authorized!',
|
||||
authFailed: 'Authorization failed',
|
||||
userCode: 'Authorization Code',
|
||||
copyCode: 'Copy Code',
|
||||
openBrowser: 'Open Browser',
|
||||
tokenLabel: 'GitHub Personal Access Token',
|
||||
tokenPlaceholder: 'Paste your GitHub Token',
|
||||
tokenHint: 'Requires repo and workflow permissions',
|
||||
createToken: 'Create Token',
|
||||
login: 'Login',
|
||||
back: 'Back'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
};
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
setAuthStatus('pending');
|
||||
setError('');
|
||||
@@ -101,7 +54,7 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
|
||||
const handleTokenAuth = async () => {
|
||||
if (!githubToken.trim()) {
|
||||
setError(locale === 'zh' ? '请输入 Token' : 'Please enter a token');
|
||||
setError(t('github.enterToken'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,7 +64,7 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
console.error('[GitHubAuth] Token auth failed:', err);
|
||||
setError(locale === 'zh' ? '认证失败,请检查你的 Token' : 'Authentication failed. Please check your token.');
|
||||
setError(t('github.authFailedToken'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,20 +83,20 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
return (
|
||||
<div className="github-auth">
|
||||
<Github size={48} style={{ color: '#0366d6' }} />
|
||||
<p>{t('githubLogin')}</p>
|
||||
<p>{t('github.githubLogin')}</p>
|
||||
|
||||
<div className="auth-tabs">
|
||||
<button
|
||||
className={`auth-tab ${useOAuth ? 'active' : ''}`}
|
||||
onClick={() => setUseOAuth(true)}
|
||||
>
|
||||
{t('oauthLogin')}
|
||||
{t('github.oauthLogin')}
|
||||
</button>
|
||||
<button
|
||||
className={`auth-tab ${!useOAuth ? 'active' : ''}`}
|
||||
onClick={() => setUseOAuth(false)}
|
||||
>
|
||||
{t('tokenLogin')}
|
||||
{t('github.tokenLogin')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -152,14 +105,14 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
{authStatus === 'idle' && (
|
||||
<>
|
||||
<div className="oauth-instructions">
|
||||
<p>{t('oauthStep1')}</p>
|
||||
<p>{t('oauthStep2')}</p>
|
||||
<p>{t('oauthStep3')}</p>
|
||||
<p>{t('github.oauthStep1')}</p>
|
||||
<p>{t('github.oauthStep2')}</p>
|
||||
<p>{t('github.oauthStep3')}</p>
|
||||
</div>
|
||||
|
||||
<button className="btn-primary" onClick={handleOAuthLogin}>
|
||||
<Github size={16} />
|
||||
{t('startAuth')}
|
||||
{t('github.startAuth')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -167,17 +120,17 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
{authStatus === 'pending' && (
|
||||
<div className="oauth-pending">
|
||||
<Loader size={48} className="spinning" style={{ color: '#0366d6' }} />
|
||||
<h4>{t('authorizing')}</h4>
|
||||
<h4>{t('github.authorizing')}</h4>
|
||||
|
||||
{userCode && (
|
||||
<div className="user-code-display">
|
||||
<label>{t('userCode')}</label>
|
||||
<label>{t('github.userCode')}</label>
|
||||
<div className="code-box">
|
||||
<span className="code-text">{userCode}</span>
|
||||
<button
|
||||
className="btn-copy"
|
||||
onClick={() => copyToClipboard(userCode)}
|
||||
title={t('copyCode')}
|
||||
title={t('github.copyCode')}
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
@@ -187,7 +140,7 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
onClick={() => open(verificationUri)}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{t('openBrowser')}
|
||||
{t('github.openBrowser')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -197,21 +150,21 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
{authStatus === 'authorized' && (
|
||||
<div className="oauth-success">
|
||||
<CheckCircle size={48} style={{ color: '#34c759' }} />
|
||||
<h4>{t('authorized')}</h4>
|
||||
<h4>{t('github.authorized')}</h4>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authStatus === 'error' && (
|
||||
<div className="oauth-error">
|
||||
<AlertCircle size={48} style={{ color: '#ff3b30' }} />
|
||||
<h4>{t('authFailed')}</h4>
|
||||
<h4>{t('github.authFailed')}</h4>
|
||||
{error && (
|
||||
<div className="error-details">
|
||||
<pre>{error}</pre>
|
||||
</div>
|
||||
)}
|
||||
<button className="btn-secondary" onClick={() => setAuthStatus('idle')}>
|
||||
{t('back')}
|
||||
{t('github.back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -219,28 +172,28 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
) : (
|
||||
<div className="token-auth">
|
||||
<div className="form-group">
|
||||
<label>{t('tokenLabel')}</label>
|
||||
<label>{t('github.tokenLabel')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={githubToken}
|
||||
onChange={(e) => setGithubToken(e.target.value)}
|
||||
placeholder={t('tokenPlaceholder')}
|
||||
placeholder={t('github.tokenPlaceholder')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleTokenAuth();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<small>{t('tokenHint')}</small>
|
||||
<small>{t('github.tokenHint')}</small>
|
||||
</div>
|
||||
|
||||
<button className="btn-link" onClick={openCreateTokenPage}>
|
||||
<ExternalLink size={14} />
|
||||
{t('createToken')}
|
||||
{t('github.createToken')}
|
||||
</button>
|
||||
|
||||
<button className="btn-primary" onClick={handleTokenAuth}>
|
||||
{t('login')}
|
||||
{t('github.login')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,32 +1,22 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import { GitHubAuth } from './GitHubAuth';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/GitHubLoginDialog.css';
|
||||
|
||||
interface GitHubLoginDialogProps {
|
||||
githubService: GitHubService;
|
||||
onClose: () => void;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function GitHubLoginDialog({ githubService, onClose, locale }: GitHubLoginDialogProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
title: 'GitHub 登录'
|
||||
},
|
||||
en: {
|
||||
title: 'GitHub Login'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
};
|
||||
export function GitHubLoginDialog({ githubService, onClose }: GitHubLoginDialogProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="github-login-overlay" onClick={onClose}>
|
||||
<div className="github-login-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="github-login-header">
|
||||
<h2>{t('title')}</h2>
|
||||
<h2>{t('github.title')}</h2>
|
||||
<button className="github-login-close" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -36,7 +26,6 @@ export function GitHubLoginDialog({ githubService, onClose, locale }: GitHubLogi
|
||||
<GitHubAuth
|
||||
githubService={githubService}
|
||||
onSuccess={onClose}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
ChevronDown
|
||||
} from 'lucide-react';
|
||||
import type { MessageHub, CommandManager } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/MainToolbar.css';
|
||||
|
||||
export type PlayState = 'stopped' | 'playing' | 'paused';
|
||||
|
||||
interface MainToolbarProps {
|
||||
locale?: string;
|
||||
messageHub?: MessageHub;
|
||||
commandManager?: CommandManager;
|
||||
onSaveScene?: () => void;
|
||||
@@ -61,7 +61,6 @@ function ToolSeparator() {
|
||||
}
|
||||
|
||||
export function MainToolbar({
|
||||
locale = 'en',
|
||||
messageHub,
|
||||
commandManager,
|
||||
onSaveScene,
|
||||
@@ -75,46 +74,13 @@ export function MainToolbar({
|
||||
onRunInBrowser,
|
||||
onRunOnDevice
|
||||
}: MainToolbarProps) {
|
||||
const { t } = useLocale();
|
||||
const [playState, setPlayState] = useState<PlayState>('stopped');
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [showRunMenu, setShowRunMenu] = useState(false);
|
||||
const runMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
stop: 'Stop',
|
||||
step: 'Step Forward',
|
||||
save: 'Save Scene (Ctrl+S)',
|
||||
open: 'Open Scene',
|
||||
undo: 'Undo (Ctrl+Z)',
|
||||
redo: 'Redo (Ctrl+Y)',
|
||||
preview: 'Preview Mode',
|
||||
runOptions: 'Run Options',
|
||||
runInBrowser: 'Run in Browser',
|
||||
runOnDevice: 'Run on Device'
|
||||
},
|
||||
zh: {
|
||||
play: '播放',
|
||||
pause: '暂停',
|
||||
stop: '停止',
|
||||
step: '单步执行',
|
||||
save: '保存场景 (Ctrl+S)',
|
||||
open: '打开场景',
|
||||
undo: '撤销 (Ctrl+Z)',
|
||||
redo: '重做 (Ctrl+Y)',
|
||||
preview: '预览模式',
|
||||
runOptions: '运行选项',
|
||||
runInBrowser: '浏览器运行',
|
||||
runOnDevice: '真机运行'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
// Close run menu when clicking outside
|
||||
useEffect(() => {
|
||||
if (!showRunMenu) return;
|
||||
@@ -228,12 +194,12 @@ export function MainToolbar({
|
||||
<div className="toolbar-group">
|
||||
<ToolButton
|
||||
icon={<Save size={16} />}
|
||||
label={t('save')}
|
||||
label={t('toolbar.save')}
|
||||
onClick={onSaveScene}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<FolderOpen size={16} />}
|
||||
label={t('open')}
|
||||
label={t('toolbar.open')}
|
||||
onClick={onOpenScene}
|
||||
/>
|
||||
</div>
|
||||
@@ -244,13 +210,13 @@ export function MainToolbar({
|
||||
<div className="toolbar-group">
|
||||
<ToolButton
|
||||
icon={<Undo2 size={16} />}
|
||||
label={t('undo')}
|
||||
label={t('toolbar.undo')}
|
||||
disabled={!canUndo}
|
||||
onClick={handleUndo}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<Redo2 size={16} />}
|
||||
label={t('redo')}
|
||||
label={t('toolbar.redo')}
|
||||
disabled={!canRedo}
|
||||
onClick={handleRedo}
|
||||
/>
|
||||
@@ -261,18 +227,18 @@ export function MainToolbar({
|
||||
<div className="toolbar-group toolbar-center">
|
||||
<ToolButton
|
||||
icon={playState === 'playing' ? <Pause size={18} /> : <Play size={18} />}
|
||||
label={playState === 'playing' ? t('pause') : t('play')}
|
||||
label={playState === 'playing' ? t('toolbar.pause') : t('toolbar.play')}
|
||||
onClick={playState === 'playing' ? handlePause : handlePlay}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<Square size={16} />}
|
||||
label={t('stop')}
|
||||
label={t('toolbar.stop')}
|
||||
disabled={playState === 'stopped'}
|
||||
onClick={handleStop}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<SkipForward size={16} />}
|
||||
label={t('step')}
|
||||
label={t('toolbar.step')}
|
||||
disabled={playState === 'playing'}
|
||||
onClick={handleStep}
|
||||
/>
|
||||
@@ -287,7 +253,7 @@ export function MainToolbar({
|
||||
e.stopPropagation();
|
||||
setShowRunMenu(prev => !prev);
|
||||
}}
|
||||
title={t('runOptions')}
|
||||
title={t('toolbar.runOptions')}
|
||||
type="button"
|
||||
>
|
||||
<Globe size={16} />
|
||||
@@ -297,11 +263,11 @@ export function MainToolbar({
|
||||
<div className="toolbar-dropdown-menu">
|
||||
<button type="button" onClick={handleRunInBrowser}>
|
||||
<Globe size={14} />
|
||||
<span>{t('runInBrowser')}</span>
|
||||
<span>{t('toolbar.runInBrowser')}</span>
|
||||
</button>
|
||||
<button type="button" onClick={handleRunOnDevice}>
|
||||
<QrCode size={14} />
|
||||
<span>{t('runOnDevice')}</span>
|
||||
<span>{t('toolbar.runOnDevice')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -314,7 +280,7 @@ export function MainToolbar({
|
||||
{playState !== 'stopped' && (
|
||||
<div className="preview-indicator">
|
||||
<Eye size={14} />
|
||||
<span>{t('preview')}</span>
|
||||
<span>{t('toolbar.preview')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { UIRegistry, MessageHub, PluginManager } from '@esengine/editor-core';
|
||||
import type { MenuItem as PluginMenuItem } from '@esengine/editor-core';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/MenuBar.css';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -15,7 +16,6 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
interface MenuBarProps {
|
||||
locale?: string;
|
||||
uiRegistry?: UIRegistry;
|
||||
messageHub?: MessageHub;
|
||||
pluginManager?: PluginManager;
|
||||
@@ -38,7 +38,6 @@ interface MenuBarProps {
|
||||
}
|
||||
|
||||
export function MenuBar({
|
||||
locale = 'en',
|
||||
uiRegistry,
|
||||
messageHub,
|
||||
pluginManager,
|
||||
@@ -59,6 +58,7 @@ export function MenuBar({
|
||||
onReloadPlugins,
|
||||
onOpenBuildSettings
|
||||
}: MenuBarProps) {
|
||||
const { t } = useLocale();
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -96,109 +96,31 @@ export function MenuBar({
|
||||
}
|
||||
}, [messageHub, uiRegistry, pluginManager]);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
file: 'File',
|
||||
newScene: 'New Scene',
|
||||
openScene: 'Open Scene',
|
||||
saveScene: 'Save Scene',
|
||||
saveSceneAs: 'Save Scene As...',
|
||||
openProject: 'Open Project',
|
||||
closeProject: 'Close Project',
|
||||
exit: 'Exit',
|
||||
edit: 'Edit',
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
cut: 'Cut',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
delete: 'Delete',
|
||||
selectAll: 'Select All',
|
||||
window: 'Window',
|
||||
sceneHierarchy: 'Scene Hierarchy',
|
||||
inspector: 'Inspector',
|
||||
assets: 'Assets',
|
||||
console: 'Console',
|
||||
viewport: 'Viewport',
|
||||
pluginManager: 'Plugin Manager',
|
||||
tools: 'Tools',
|
||||
createPlugin: 'Create Plugin',
|
||||
reloadPlugins: 'Reload Plugins',
|
||||
portManager: 'Port Manager',
|
||||
settings: 'Settings',
|
||||
help: 'Help',
|
||||
documentation: 'Documentation',
|
||||
checkForUpdates: 'Check for Updates',
|
||||
about: 'About',
|
||||
devtools: 'Developer Tools',
|
||||
buildSettings: 'Build Settings'
|
||||
},
|
||||
zh: {
|
||||
file: '文件',
|
||||
newScene: '新建场景',
|
||||
openScene: '打开场景',
|
||||
saveScene: '保存场景',
|
||||
saveSceneAs: '场景另存为...',
|
||||
openProject: '打开项目',
|
||||
closeProject: '关闭项目',
|
||||
exit: '退出',
|
||||
edit: '编辑',
|
||||
undo: '撤销',
|
||||
redo: '重做',
|
||||
cut: '剪切',
|
||||
copy: '复制',
|
||||
paste: '粘贴',
|
||||
delete: '删除',
|
||||
selectAll: '全选',
|
||||
window: '窗口',
|
||||
sceneHierarchy: '场景层级',
|
||||
inspector: '检视器',
|
||||
assets: '资产',
|
||||
console: '控制台',
|
||||
viewport: '视口',
|
||||
pluginManager: '插件管理器',
|
||||
tools: '工具',
|
||||
createPlugin: '创建插件',
|
||||
reloadPlugins: '重新加载插件',
|
||||
portManager: '端口管理器',
|
||||
settings: '设置',
|
||||
help: '帮助',
|
||||
documentation: '文档',
|
||||
checkForUpdates: '检查更新',
|
||||
about: '关于',
|
||||
devtools: '开发者工具',
|
||||
buildSettings: '构建设置'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
const menus: Record<string, MenuItem[]> = {
|
||||
file: [
|
||||
{ label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ label: t('menu.file.newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('menu.file.openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ separator: true },
|
||||
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ label: t('menu.file.saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('menu.file.saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ separator: true },
|
||||
{ label: t('buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ label: t('menu.file.buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ separator: true },
|
||||
{ label: t('openProject'), onClick: onOpenProject },
|
||||
{ label: t('closeProject'), onClick: onCloseProject },
|
||||
{ label: t('menu.file.openProject'), onClick: onOpenProject },
|
||||
{ label: t('menu.file.closeProject'), onClick: onCloseProject },
|
||||
{ separator: true },
|
||||
{ label: t('exit'), onClick: onExit }
|
||||
{ label: t('menu.file.exit'), onClick: onExit }
|
||||
],
|
||||
edit: [
|
||||
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ label: t('menu.edit.undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('menu.edit.redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('delete'), shortcut: 'Delete', disabled: true },
|
||||
{ label: t('menu.edit.cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('menu.edit.copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('menu.edit.paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('menu.edit.delete'), shortcut: 'Delete', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
{ label: t('menu.edit.selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
],
|
||||
window: [
|
||||
...pluginMenuItems.map((item) => ({
|
||||
@@ -208,25 +130,34 @@ export function MenuBar({
|
||||
onClick: item.onClick
|
||||
})),
|
||||
...(pluginMenuItems.length > 0 ? [{ separator: true } as MenuItem] : []),
|
||||
{ label: t('pluginManager'), onClick: onOpenPluginManager },
|
||||
{ label: t('menu.window.pluginManager'), onClick: onOpenPluginManager },
|
||||
{ separator: true },
|
||||
{ label: t('devtools'), onClick: onToggleDevtools }
|
||||
{ label: t('menu.window.devtools'), onClick: onToggleDevtools }
|
||||
],
|
||||
tools: [
|
||||
{ label: t('createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ label: t('menu.tools.createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('menu.tools.reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ separator: true },
|
||||
{ label: t('portManager'), onClick: onOpenPortManager },
|
||||
{ label: t('menu.tools.portManager'), onClick: onOpenPortManager },
|
||||
{ separator: true },
|
||||
{ label: t('settings'), onClick: onOpenSettings }
|
||||
{ label: t('menu.tools.settings'), onClick: onOpenSettings }
|
||||
],
|
||||
help: [
|
||||
{ label: t('documentation'), disabled: true },
|
||||
{ label: t('menu.help.documentation'), disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('about'), onClick: onOpenAbout }
|
||||
{ label: t('menu.help.about'), onClick: onOpenAbout }
|
||||
]
|
||||
};
|
||||
|
||||
// 菜单键到翻译键的映射 | Map menu keys to translation keys
|
||||
const menuTitleKeys: Record<string, string> = {
|
||||
file: 'menu.file.title',
|
||||
edit: 'menu.edit.title',
|
||||
window: 'menu.window.title',
|
||||
tools: 'menu.tools.title',
|
||||
help: 'menu.help.title'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
@@ -259,7 +190,7 @@ export function MenuBar({
|
||||
className={`menu-button ${openMenu === menuKey ? 'active' : ''}`}
|
||||
onClick={() => handleMenuClick(menuKey)}
|
||||
>
|
||||
{t(menuKey)}
|
||||
{t(menuTitleKeys[menuKey] || menuKey)}
|
||||
</button>
|
||||
{openMenu === menuKey && menus[menuKey] && (
|
||||
<div className="menu-dropdown">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Search, Filter, Settings, X, Trash2, ChevronDown,
|
||||
Bug, Info, AlertTriangle, XCircle, AlertCircle, Wifi, Pause, Play, Copy
|
||||
} from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/OutputLogPanel.css';
|
||||
|
||||
interface OutputLogPanelProps {
|
||||
@@ -145,6 +146,7 @@ const LogEntryItem = memo(({ log, isExpanded, onToggle, onCopy }: {
|
||||
LogEntryItem.displayName = 'LogEntryItem';
|
||||
|
||||
export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLogPanelProps) {
|
||||
const { t } = useLocale();
|
||||
const [logs, setLogs] = useState<LogEntry[]>(() => logService.getLogs().slice(-MAX_LOGS));
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [levelFilter, setLevelFilter] = useState<Set<LogLevel>>(new Set([
|
||||
@@ -279,7 +281,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={locale === 'zh' ? '搜索日志...' : 'Search logs...'}
|
||||
placeholder={t('outputLog.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -305,7 +307,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
}}
|
||||
>
|
||||
<Filter size={14} />
|
||||
<span>{locale === 'zh' ? '过滤器' : 'Filters'}</span>
|
||||
<span>{t('outputLog.filters')}</span>
|
||||
{activeFilterCount > 0 && (
|
||||
<span className="filter-badge">{activeFilterCount}</span>
|
||||
)}
|
||||
@@ -314,7 +316,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
{showFilterMenu && (
|
||||
<div className="output-log-menu">
|
||||
<div className="output-log-menu-header">
|
||||
{locale === 'zh' ? '日志级别' : 'Log Levels'}
|
||||
{t('outputLog.logLevels')}
|
||||
</div>
|
||||
<label className="output-log-menu-item">
|
||||
<input
|
||||
@@ -364,7 +366,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
onChange={() => setShowRemoteOnly(!showRemoteOnly)}
|
||||
/>
|
||||
<Wifi size={14} className="level-icon remote" />
|
||||
<span>{locale === 'zh' ? '仅远程日志' : 'Remote Only'}</span>
|
||||
<span>{t('outputLog.remoteOnly')}</span>
|
||||
<span className="level-count">{remoteLogCount}</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -376,8 +378,8 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
className={`output-log-icon-btn ${autoScroll ? 'active' : ''}`}
|
||||
onClick={() => setAutoScroll(!autoScroll)}
|
||||
title={autoScroll
|
||||
? (locale === 'zh' ? '暂停自动滚动' : 'Pause auto-scroll')
|
||||
: (locale === 'zh' ? '恢复自动滚动' : 'Resume auto-scroll')
|
||||
? t('outputLog.pauseAutoScroll')
|
||||
: t('outputLog.resumeAutoScroll')
|
||||
}
|
||||
>
|
||||
{autoScroll ? <Pause size={14} /> : <Play size={14} />}
|
||||
@@ -391,7 +393,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
setShowSettingsMenu(!showSettingsMenu);
|
||||
setShowFilterMenu(false);
|
||||
}}
|
||||
title={locale === 'zh' ? '设置' : 'Settings'}
|
||||
title={t('outputLog.settings')}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
@@ -402,7 +404,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
onClick={handleClear}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>{locale === 'zh' ? '清空日志' : 'Clear Logs'}</span>
|
||||
<span>{t('outputLog.clearLogs')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -430,8 +432,8 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
<div className="output-log-empty">
|
||||
<AlertCircle size={32} />
|
||||
<p>{searchQuery
|
||||
? (locale === 'zh' ? '没有匹配的日志' : 'No matching logs')
|
||||
: (locale === 'zh' ? '暂无日志' : 'No logs to display')
|
||||
? t('outputLog.noMatchingLogs')
|
||||
: t('outputLog.noLogs')
|
||||
}</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -449,7 +451,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="output-log-status">
|
||||
<span>{filteredLogs.length} / {logs.length} {locale === 'zh' ? '条日志' : 'logs'}</span>
|
||||
<span>{filteredLogs.length} / {logs.length} {t('outputLog.logs')}</span>
|
||||
{!autoScroll && (
|
||||
<button
|
||||
className="output-log-scroll-btn"
|
||||
@@ -460,7 +462,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
}
|
||||
}}
|
||||
>
|
||||
↓ {locale === 'zh' ? '滚动到底部' : 'Scroll to bottom'}
|
||||
↓ {t('outputLog.scrollToBottom')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { X, FolderOpen } from 'lucide-react';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/PluginGeneratorWindow.css';
|
||||
|
||||
interface PluginGeneratorWindowProps {
|
||||
onClose: () => void;
|
||||
projectPath: string | null;
|
||||
locale: string;
|
||||
onSuccess?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess }: PluginGeneratorWindowProps) {
|
||||
export function PluginGeneratorWindow({ onClose, projectPath, onSuccess }: PluginGeneratorWindowProps) {
|
||||
const { t } = useLocale();
|
||||
const [pluginName, setPluginName] = useState('');
|
||||
const [pluginVersion, setPluginVersion] = useState('1.0.0');
|
||||
const [outputPath, setOutputPath] = useState(projectPath ? `${projectPath}/plugins` : '');
|
||||
@@ -18,44 +19,6 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
title: '创建插件',
|
||||
pluginName: '插件名称',
|
||||
pluginNamePlaceholder: '例如: my-game-plugin',
|
||||
pluginVersion: '插件版本',
|
||||
outputPath: '输出路径',
|
||||
selectPath: '选择路径',
|
||||
includeExample: '包含示例节点',
|
||||
generate: '生成插件',
|
||||
cancel: '取消',
|
||||
generating: '正在生成...',
|
||||
success: '插件创建成功!',
|
||||
errorEmpty: '请输入插件名称',
|
||||
errorInvalidName: '插件名称只能包含字母、数字、连字符和下划线',
|
||||
errorNoPath: '请选择输出路径'
|
||||
},
|
||||
en: {
|
||||
title: 'Create Plugin',
|
||||
pluginName: 'Plugin Name',
|
||||
pluginNamePlaceholder: 'e.g: my-game-plugin',
|
||||
pluginVersion: 'Plugin Version',
|
||||
outputPath: 'Output Path',
|
||||
selectPath: 'Select Path',
|
||||
includeExample: 'Include Example Node',
|
||||
generate: 'Generate Plugin',
|
||||
cancel: 'Cancel',
|
||||
generating: 'Generating...',
|
||||
success: 'Plugin created successfully!',
|
||||
errorEmpty: 'Please enter plugin name',
|
||||
errorInvalidName: 'Plugin name can only contain letters, numbers, hyphens and underscores',
|
||||
errorNoPath: 'Please select output path'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
};
|
||||
|
||||
const handleSelectPath = async () => {
|
||||
try {
|
||||
const selected = await TauriAPI.openProjectDialog();
|
||||
@@ -69,11 +32,11 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
|
||||
const validatePluginName = (name: string): boolean => {
|
||||
if (!name) {
|
||||
setError(t('errorEmpty'));
|
||||
setError(t('pluginGenerator.errorEmpty'));
|
||||
return false;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(name)) {
|
||||
setError(t('errorInvalidName'));
|
||||
setError(t('pluginGenerator.errorInvalidName'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -87,7 +50,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
}
|
||||
|
||||
if (!outputPath) {
|
||||
setError(t('errorNoPath'));
|
||||
setError(t('pluginGenerator.errorNoPath'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,7 +77,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
alert(t('success'));
|
||||
alert(t('pluginGenerator.success'));
|
||||
|
||||
if (result.path) {
|
||||
await TauriAPI.showInFolder(result.path);
|
||||
@@ -137,7 +100,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content plugin-generator-window">
|
||||
<div className="modal-header">
|
||||
<h2>{t('title')}</h2>
|
||||
<h2>{t('pluginGenerator.title')}</h2>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
@@ -145,18 +108,18 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="form-group">
|
||||
<label>{t('pluginName')}</label>
|
||||
<label>{t('pluginGenerator.pluginName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pluginName}
|
||||
onChange={(e) => setPluginName(e.target.value)}
|
||||
placeholder={t('pluginNamePlaceholder')}
|
||||
placeholder={t('pluginGenerator.pluginNamePlaceholder')}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t('pluginVersion')}</label>
|
||||
<label>{t('pluginGenerator.pluginVersion')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pluginVersion}
|
||||
@@ -166,7 +129,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t('outputPath')}</label>
|
||||
<label>{t('pluginGenerator.outputPath')}</label>
|
||||
<div className="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
@@ -180,7 +143,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
disabled={isGenerating}
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
{t('selectPath')}
|
||||
{t('pluginGenerator.selectPath')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +156,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
onChange={(e) => setIncludeExample(e.target.checked)}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<span>{t('includeExample')}</span>
|
||||
<span>{t('pluginGenerator.includeExample')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -210,14 +173,14 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{isGenerating ? t('generating') : t('generate')}
|
||||
{isGenerating ? t('pluginGenerator.generating') : t('pluginGenerator.generate')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={onClose}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t('pluginGenerator.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { Folder, Sparkles, X } from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/ProjectCreationWizard.css';
|
||||
|
||||
// 项目模板类型
|
||||
// 项目模板类型(使用翻译键)
|
||||
// Project template type (using translation keys)
|
||||
interface ProjectTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
nameZh: string;
|
||||
description: string;
|
||||
descriptionZh: string;
|
||||
nameKey: string; // 翻译键 | Translation key
|
||||
descriptionKey: string;
|
||||
}
|
||||
|
||||
const templates: ProjectTemplate[] = [
|
||||
{
|
||||
id: 'blank',
|
||||
name: 'Blank',
|
||||
nameZh: '空白',
|
||||
description: 'A blank project with no starter content. Perfect for starting from scratch.',
|
||||
descriptionZh: '不包含任何启动内容的空白项目,适合从零开始创建。'
|
||||
nameKey: 'project.wizard.templates.blank',
|
||||
descriptionKey: 'project.wizard.templates.blankDesc'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -34,30 +32,16 @@ export function ProjectCreationWizard({
|
||||
onClose,
|
||||
onCreateProject,
|
||||
onBrowsePath,
|
||||
locale
|
||||
locale: _locale
|
||||
}: ProjectCreationWizardProps) {
|
||||
const { t } = useLocale();
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('blank');
|
||||
const [projectName, setProjectName] = useState('MyProject');
|
||||
const [projectPath, setProjectPath] = useState('');
|
||||
|
||||
const t = {
|
||||
title: locale === 'zh' ? '项目浏览器' : 'Project Browser',
|
||||
recentProjects: locale === 'zh' ? '最近打开的项目' : 'Recent Projects',
|
||||
newProject: locale === 'zh' ? '新建项目' : 'New Project',
|
||||
projectName: locale === 'zh' ? '项目名称' : 'Project Name',
|
||||
projectLocation: locale === 'zh' ? '项目位置' : 'Project Location',
|
||||
browse: locale === 'zh' ? '浏览...' : 'Browse...',
|
||||
create: locale === 'zh' ? '创建' : 'Create',
|
||||
cancel: locale === 'zh' ? '取消' : 'Cancel',
|
||||
selectTemplate: locale === 'zh' ? '选择模板' : 'Select a Template',
|
||||
projectSettings: locale === 'zh' ? '项目设置' : 'Project Settings',
|
||||
blank: locale === 'zh' ? '空白' : 'Blank',
|
||||
blankDesc: locale === 'zh' ? '不含任何代码的空白项目。' : 'A blank project with no code.'
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const currentTemplate = templates.find(t => t.id === selectedTemplate);
|
||||
const currentTemplate = templates.find(tmpl => tmpl.id === selectedTemplate);
|
||||
|
||||
const handleBrowse = async () => {
|
||||
const path = await onBrowsePath();
|
||||
@@ -77,7 +61,7 @@ export function ProjectCreationWizard({
|
||||
<div className="project-wizard-overlay">
|
||||
<div className="project-wizard">
|
||||
<div className="wizard-header">
|
||||
<h1>{t.title}</h1>
|
||||
<h1>{t('project.wizard.title')}</h1>
|
||||
<button className="wizard-close-btn" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -87,7 +71,7 @@ export function ProjectCreationWizard({
|
||||
{/* Templates grid */}
|
||||
<div className="wizard-templates">
|
||||
<div className="templates-header">
|
||||
<h3>{t.selectTemplate}</h3>
|
||||
<h3>{t('project.wizard.selectTemplate')}</h3>
|
||||
</div>
|
||||
<div className="templates-grid">
|
||||
{templates.map(template => (
|
||||
@@ -100,7 +84,7 @@ export function ProjectCreationWizard({
|
||||
<Sparkles size={32} />
|
||||
</div>
|
||||
<div className="template-name">
|
||||
{locale === 'zh' ? template.nameZh : template.name}
|
||||
{t(template.nameKey)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -116,15 +100,15 @@ export function ProjectCreationWizard({
|
||||
</div>
|
||||
|
||||
<div className="details-info">
|
||||
<h2>{locale === 'zh' ? currentTemplate?.nameZh : currentTemplate?.name}</h2>
|
||||
<p>{locale === 'zh' ? currentTemplate?.descriptionZh : currentTemplate?.description}</p>
|
||||
<h2>{currentTemplate ? t(currentTemplate.nameKey) : ''}</h2>
|
||||
<p>{currentTemplate ? t(currentTemplate.descriptionKey) : ''}</p>
|
||||
</div>
|
||||
|
||||
<div className="details-settings">
|
||||
<h3>{t.projectSettings}</h3>
|
||||
<h3>{t('project.wizard.projectSettings')}</h3>
|
||||
|
||||
<div className="setting-field">
|
||||
<label>{t.projectName}</label>
|
||||
<label>{t('project.wizard.projectName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={projectName}
|
||||
@@ -134,7 +118,7 @@ export function ProjectCreationWizard({
|
||||
</div>
|
||||
|
||||
<div className="setting-field">
|
||||
<label>{t.projectLocation}</label>
|
||||
<label>{t('project.wizard.projectLocation')}</label>
|
||||
<div className="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
@@ -153,14 +137,14 @@ export function ProjectCreationWizard({
|
||||
|
||||
<div className="wizard-footer">
|
||||
<button className="wizard-btn secondary" onClick={onClose}>
|
||||
{t.cancel}
|
||||
{t('project.wizard.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="wizard-btn primary"
|
||||
onClick={handleCreate}
|
||||
disabled={!projectName || !projectPath}
|
||||
>
|
||||
{t.create}
|
||||
{t('project.wizard.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -480,11 +480,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
if (!entity) return;
|
||||
|
||||
const confirmed = await confirm(
|
||||
locale === 'zh'
|
||||
? `确定要删除实体 "${entity.name}" 吗?`
|
||||
: `Are you sure you want to delete entity "${entity.name}"?`,
|
||||
t('hierarchy.deleteConfirm', { name: entity.name }),
|
||||
{
|
||||
title: locale === 'zh' ? '删除实体' : 'Delete Entity',
|
||||
title: t('hierarchy.deleteEntity'),
|
||||
kind: 'warning'
|
||||
}
|
||||
);
|
||||
@@ -544,7 +542,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
*/
|
||||
const handleCreateFolder = useCallback(() => {
|
||||
const entityCount = entityStore.getAllEntities().length;
|
||||
const folderName = locale === 'zh' ? `文件夹 ${entityCount + 1}` : `Folder ${entityCount + 1}`;
|
||||
const folderName = `Folder ${entityCount + 1}`;
|
||||
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
@@ -656,7 +654,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={locale === 'zh' ? '搜索...' : 'Search...'}
|
||||
placeholder={t('hierarchy.search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -669,14 +667,14 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<button
|
||||
className="outliner-action-btn"
|
||||
onClick={handleCreateEntity}
|
||||
title={locale === 'zh' ? '创建实体' : 'Create Entity'}
|
||||
title={t('hierarchy.createEntity')}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="outliner-action-btn"
|
||||
onClick={handleCreateFolder}
|
||||
title={locale === 'zh' ? '创建文件夹' : 'Create Folder'}
|
||||
title={t('hierarchy.createFolder')}
|
||||
>
|
||||
<FolderPlus size={14} />
|
||||
</button>
|
||||
@@ -684,7 +682,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
)}
|
||||
<button
|
||||
className="outliner-action-btn"
|
||||
title={locale === 'zh' ? '设置' : 'Settings'}
|
||||
title={t('hierarchy.settings')}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
@@ -695,14 +693,14 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<button
|
||||
className={`mode-btn ${viewMode === 'local' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('local')}
|
||||
title={locale === 'zh' ? '本地场景' : 'Local Scene'}
|
||||
title={t('hierarchy.localScene')}
|
||||
>
|
||||
<Monitor size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`mode-btn ${viewMode === 'remote' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('remote')}
|
||||
title={locale === 'zh' ? '远程实体' : 'Remote Entities'}
|
||||
title={t('hierarchy.remoteEntities')}
|
||||
>
|
||||
<Globe size={14} />
|
||||
</button>
|
||||
@@ -719,9 +717,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
{/* Column Headers */}
|
||||
<div className="outliner-header">
|
||||
<div className="outliner-header-icons">
|
||||
<span title={locale === 'zh' ? '可见性' : 'Visibility'}><Eye size={12} className="header-icon" /></span>
|
||||
<span title={locale === 'zh' ? '收藏' : 'Favorite'}><Star size={12} className="header-icon" /></span>
|
||||
<span title={locale === 'zh' ? '锁定' : 'Lock'}><Lock size={12} className="header-icon" /></span>
|
||||
<span title={t('hierarchy.visibility')}><Eye size={12} className="header-icon" /></span>
|
||||
<span title={t('hierarchy.favorite')}><Star size={12} className="header-icon" /></span>
|
||||
<span title={t('hierarchy.lock')}><Lock size={12} className="header-icon" /></span>
|
||||
</div>
|
||||
<div
|
||||
className={`outliner-header-label ${sortColumn === 'name' ? 'sorted' : ''}`}
|
||||
@@ -751,7 +749,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<div className="empty-state">
|
||||
<Box size={32} strokeWidth={1.5} className="empty-icon" />
|
||||
<div className="empty-hint">
|
||||
{locale === 'zh' ? '远程游戏中没有实体' : 'No entities in remote game'}
|
||||
{t('hierarchy.remoteEmpty')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -783,7 +781,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<div className="empty-state">
|
||||
<Box size={32} strokeWidth={1.5} className="empty-icon" />
|
||||
<div className="empty-hint">
|
||||
{locale === 'zh' ? '创建实体开始使用' : 'Create an entity to get started'}
|
||||
{t('hierarchy.emptyHint')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -873,9 +871,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="outliner-status">
|
||||
<span>{totalCount} {locale === 'zh' ? '个对象' : 'actors'}</span>
|
||||
<span>{totalCount} {t('hierarchy.actors')}</span>
|
||||
{selectedCount > 0 && (
|
||||
<span> ({selectedCount} {locale === 'zh' ? '个已选中' : 'selected'})</span>
|
||||
<span> ({selectedCount} {t('hierarchy.selected')})</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -884,6 +882,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
locale={locale}
|
||||
t={t}
|
||||
entityId={contextMenu.entityId}
|
||||
pluginTemplates={pluginTemplates}
|
||||
onCreateEmpty={() => { handleCreateEntity(); closeContextMenu(); }}
|
||||
@@ -904,6 +903,7 @@ interface ContextMenuWithSubmenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
locale: string;
|
||||
t: (key: string, params?: Record<string, string | number>, fallback?: string) => string;
|
||||
entityId: number | null;
|
||||
pluginTemplates: EntityCreationTemplate[];
|
||||
onCreateEmpty: () => void;
|
||||
@@ -914,43 +914,40 @@ interface ContextMenuWithSubmenuProps {
|
||||
}
|
||||
|
||||
function ContextMenuWithSubmenu({
|
||||
x, y, locale, entityId, pluginTemplates,
|
||||
x, y, locale, t, entityId, pluginTemplates,
|
||||
onCreateEmpty, onCreateFolder, onCreateFromTemplate, onDelete
|
||||
}: ContextMenuWithSubmenuProps) {
|
||||
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||
const [submenuPosition, setSubmenuPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const categoryLabels: Record<string, { zh: string; en: string }> = {
|
||||
'basic': { zh: '基础', en: 'Basic' },
|
||||
'rendering': { zh: '2D 对象', en: '2D Objects' },
|
||||
'ui': { zh: 'UI', en: 'UI' },
|
||||
'physics': { zh: '物理', en: 'Physics' },
|
||||
'audio': { zh: '音频', en: 'Audio' },
|
||||
'effects': { zh: '特效', en: 'Effects' },
|
||||
'other': { zh: '其他', en: 'Other' },
|
||||
};
|
||||
|
||||
// 实体创建模板的 label 本地化映射
|
||||
const entityTemplateLabels: Record<string, { zh: string; en: string }> = {
|
||||
'Sprite': { zh: '精灵', en: 'Sprite' },
|
||||
'Animated Sprite': { zh: '动画精灵', en: 'Animated Sprite' },
|
||||
'创建 Tilemap': { zh: '瓦片地图', en: 'Tilemap' },
|
||||
'Camera 2D': { zh: '2D 相机', en: 'Camera 2D' },
|
||||
'创建粒子效果': { zh: '粒子效果', en: 'Particle Effect' },
|
||||
};
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels = categoryLabels[category];
|
||||
return labels ? (locale === 'zh' ? labels.zh : labels.en) : category;
|
||||
// Map category keys to translation keys
|
||||
const categoryKeyMap: Record<string, string> = {
|
||||
'rendering': 'hierarchy.categories.rendering',
|
||||
'ui': 'hierarchy.categories.ui',
|
||||
'effects': 'hierarchy.categories.effects',
|
||||
'physics': 'hierarchy.categories.physics',
|
||||
'audio': 'hierarchy.categories.audio',
|
||||
'basic': 'hierarchy.categories.basic',
|
||||
'other': 'hierarchy.categories.other'
|
||||
};
|
||||
const key = categoryKeyMap[category];
|
||||
return key ? t(key) : category;
|
||||
};
|
||||
|
||||
const getEntityTemplateLabel = (label: string) => {
|
||||
const mapping = entityTemplateLabels[label];
|
||||
if (mapping) {
|
||||
return locale === 'zh' ? mapping.zh : mapping.en;
|
||||
}
|
||||
return label;
|
||||
// Map template labels to translation keys
|
||||
const templateKeyMap: Record<string, string> = {
|
||||
'Sprite': 'hierarchy.entityTemplates.sprite',
|
||||
'Animated Sprite': 'hierarchy.entityTemplates.animatedSprite',
|
||||
'创建 Tilemap': 'hierarchy.entityTemplates.tilemap',
|
||||
'Camera 2D': 'hierarchy.entityTemplates.camera2d',
|
||||
'创建粒子效果': 'hierarchy.entityTemplates.particleEffect'
|
||||
};
|
||||
const key = templateKeyMap[label];
|
||||
return key ? t(key) : label;
|
||||
};
|
||||
|
||||
const templatesByCategory = pluginTemplates.reduce((acc, template) => {
|
||||
@@ -985,12 +982,12 @@ function ContextMenuWithSubmenu({
|
||||
>
|
||||
<button onClick={onCreateEmpty}>
|
||||
<Plus size={12} />
|
||||
<span>{locale === 'zh' ? '创建空实体' : 'Create Empty Entity'}</span>
|
||||
<span>{t('hierarchy.createEmptyEntity')}</span>
|
||||
</button>
|
||||
|
||||
<button onClick={onCreateFolder}>
|
||||
<Folder size={12} />
|
||||
<span>{locale === 'zh' ? '创建文件夹' : 'Create Folder'}</span>
|
||||
<span>{t('hierarchy.createFolder')}</span>
|
||||
</button>
|
||||
|
||||
{sortedCategories.length > 0 && <div className="context-menu-divider" />}
|
||||
@@ -1029,7 +1026,7 @@ function ContextMenuWithSubmenu({
|
||||
<div className="context-menu-divider" />
|
||||
<button onClick={onDelete} className="context-menu-danger">
|
||||
<Trash2 size={12} />
|
||||
<span>{locale === 'zh' ? '删除实体' : 'Delete Entity'}</span>
|
||||
<span>{t('hierarchy.deleteEntity')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager, ModuleManifest } from '@esengine/editor-core';
|
||||
import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager, ModuleManifest, isTranslationKey, getTranslationKey } from '@esengine/editor-core';
|
||||
import { PluginListSetting } from './PluginListSetting';
|
||||
import { ModuleListSetting } from './ModuleListSetting';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/SettingsWindow.css';
|
||||
|
||||
interface SettingsWindowProps {
|
||||
@@ -32,59 +33,87 @@ interface MainCategory {
|
||||
}
|
||||
|
||||
export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: SettingsWindowProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
/**
|
||||
* Resolve localizable text - if it starts with '$', treat as translation key
|
||||
* 解析可本地化文本 - 如果以 '$' 开头,作为翻译键处理
|
||||
*/
|
||||
const resolveText = (text: string | undefined): string => {
|
||||
if (!text) return '';
|
||||
if (isTranslationKey(text)) {
|
||||
return t(getTranslationKey(text));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const [categories, setCategories] = useState<SettingCategory[]>([]);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(initialCategoryId || null);
|
||||
const [values, setValues] = useState<Map<string, any>>(new Map());
|
||||
const [errors, setErrors] = useState<Map<string, string>>(new Map());
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||
const [expandedMainCategories, setExpandedMainCategories] = useState<Set<string>>(new Set(['通用']));
|
||||
const [expandedMainCategories, setExpandedMainCategories] = useState<Set<string>>(new Set(['general']));
|
||||
|
||||
// 将分类组织成主分类和子分类
|
||||
// Organize categories into main categories and sub-categories
|
||||
const mainCategories = useMemo((): MainCategory[] => {
|
||||
const categoryMap = new Map<string, SettingCategory[]>();
|
||||
|
||||
// 定义主分类映射
|
||||
// 定义主分类映射(使用配置 ID 作为键)
|
||||
// Main category mapping (using config IDs as keys)
|
||||
const mainCategoryMapping: Record<string, string> = {
|
||||
'appearance': '通用',
|
||||
'general': '通用',
|
||||
'project': '通用',
|
||||
'plugins': '通用',
|
||||
'editor': '通用',
|
||||
'physics': '全局',
|
||||
'rendering': '全局',
|
||||
'audio': '全局',
|
||||
'world': '世界分区',
|
||||
'local': '世界分区(本地)',
|
||||
'performance': '性能'
|
||||
'appearance': 'general',
|
||||
'general': 'general',
|
||||
'project': 'general',
|
||||
'plugins': 'general',
|
||||
'editor': 'general',
|
||||
'physics': 'global',
|
||||
'rendering': 'global',
|
||||
'audio': 'global',
|
||||
'world': 'worldPartition',
|
||||
'local': 'worldPartitionLocal',
|
||||
'performance': 'performance'
|
||||
};
|
||||
|
||||
categories.forEach((cat) => {
|
||||
const mainCatName = mainCategoryMapping[cat.id] || '其他';
|
||||
if (!categoryMap.has(mainCatName)) {
|
||||
categoryMap.set(mainCatName, []);
|
||||
const mainCatId = mainCategoryMapping[cat.id] || 'other';
|
||||
if (!categoryMap.has(mainCatId)) {
|
||||
categoryMap.set(mainCatId, []);
|
||||
}
|
||||
categoryMap.get(mainCatName)!.push(cat);
|
||||
categoryMap.get(mainCatId)!.push(cat);
|
||||
});
|
||||
|
||||
// 定义固定的主分类顺序
|
||||
// 定义固定的主分类顺序(使用配置 ID)
|
||||
// Define fixed main category order (using config IDs)
|
||||
const orderedMainCategories = [
|
||||
'通用',
|
||||
'全局',
|
||||
'世界分区',
|
||||
'世界分区(本地)',
|
||||
'性能',
|
||||
'其他'
|
||||
'general',
|
||||
'global',
|
||||
'worldPartition',
|
||||
'worldPartitionLocal',
|
||||
'performance',
|
||||
'other'
|
||||
];
|
||||
|
||||
// 主分类 ID 到翻译键的映射
|
||||
// Main category ID to translation key mapping
|
||||
const categoryTranslationKeys: Record<string, string> = {
|
||||
'general': 'settingsWindow.mainCategories.general',
|
||||
'global': 'settingsWindow.mainCategories.global',
|
||||
'worldPartition': 'settingsWindow.mainCategories.worldPartition',
|
||||
'worldPartitionLocal': 'settingsWindow.mainCategories.worldPartitionLocal',
|
||||
'performance': 'settingsWindow.mainCategories.performance',
|
||||
'other': 'settingsWindow.mainCategories.other'
|
||||
};
|
||||
|
||||
return orderedMainCategories
|
||||
.filter((name) => categoryMap.has(name))
|
||||
.map((name) => ({
|
||||
id: name,
|
||||
title: name,
|
||||
subCategories: categoryMap.get(name)!
|
||||
.filter((id) => categoryMap.has(id))
|
||||
.map((id) => ({
|
||||
id,
|
||||
title: t(categoryTranslationKeys[id] || 'settingsWindow.mainCategories.other'),
|
||||
subCategories: categoryMap.get(id)!
|
||||
}));
|
||||
}, [categories]);
|
||||
}, [categories, t]);
|
||||
|
||||
// 获取显示的子分类标题
|
||||
const subCategoryTitle = useMemo(() => {
|
||||
@@ -170,9 +199,9 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
|
||||
const newErrors = new Map(errors);
|
||||
if (!settingsRegistry.validateSetting(descriptor, value)) {
|
||||
newErrors.set(key, descriptor.validator?.errorMessage || '无效值');
|
||||
newErrors.set(key, descriptor.validator?.errorMessage || t('settingsWindow.invalidValue'));
|
||||
setErrors(newErrors);
|
||||
return; // 验证失败,不保存
|
||||
return; // 验证失败,不保存 | Validation failed, don't save
|
||||
} else {
|
||||
newErrors.delete(key);
|
||||
}
|
||||
@@ -334,7 +363,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -354,7 +383,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -362,7 +391,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
className={`settings-number-input ${error ? 'error' : ''}`}
|
||||
value={value}
|
||||
onChange={(e) => handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)}
|
||||
placeholder={setting.placeholder}
|
||||
placeholder={resolveText(setting.placeholder)}
|
||||
min={setting.min}
|
||||
max={setting.max}
|
||||
step={setting.step}
|
||||
@@ -378,7 +407,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -386,7 +415,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
className={`settings-text-input ${error ? 'error' : ''}`}
|
||||
value={value}
|
||||
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
|
||||
placeholder={setting.placeholder}
|
||||
placeholder={resolveText(setting.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,7 +428,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<select
|
||||
@@ -414,7 +443,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
>
|
||||
{setting.options?.map((option) => (
|
||||
<option key={String(option.value)} value={String(option.value)}>
|
||||
{option.label}
|
||||
{resolveText(option.label)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -429,7 +458,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -453,7 +482,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<div className="settings-color-bar" style={{ backgroundColor: value }}>
|
||||
@@ -473,7 +502,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
if (!pluginManager) {
|
||||
return (
|
||||
<div className="settings-row">
|
||||
<p className="settings-error">PluginManager 不可用</p>
|
||||
<p className="settings-error">{t('settingsWindow.pluginManagerUnavailable')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -495,7 +524,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
}
|
||||
return (
|
||||
<div className="settings-row">
|
||||
<p className="settings-hint">碰撞矩阵编辑器未配置</p>
|
||||
<p className="settings-hint">{t('settingsWindow.collisionMatrixNotConfigured')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -539,14 +568,14 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
<div className="settings-sidebar-new">
|
||||
<div className="settings-sidebar-header">
|
||||
<SettingsIcon size={16} />
|
||||
<span>编辑器偏好设置</span>
|
||||
<span>{t('settingsWindow.editorPreferences')}</span>
|
||||
<button className="settings-sidebar-close" onClick={handleCancel}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="settings-sidebar-search">
|
||||
<span>所有设置</span>
|
||||
<span>{t('settingsWindow.allSettings')}</span>
|
||||
</div>
|
||||
|
||||
<div className="settings-sidebar-categories">
|
||||
@@ -572,7 +601,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
className={`settings-sub-category ${selectedCategoryId === subCat.id ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCategoryId(subCat.id)}
|
||||
>
|
||||
{subCat.title}
|
||||
{resolveText(subCat.title)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -590,20 +619,20 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索"
|
||||
placeholder={t('settingsWindow.search')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-header-actions">
|
||||
<button className="settings-icon-btn" title="设置">
|
||||
<button className="settings-icon-btn" title={t('settingsWindow.settingsBtn')}>
|
||||
<SettingsIcon size={14} />
|
||||
</button>
|
||||
<button className="settings-action-btn" onClick={handleExport}>
|
||||
导出......
|
||||
{t('settingsWindow.export')}
|
||||
</button>
|
||||
<button className="settings-action-btn" onClick={handleImport}>
|
||||
导入......
|
||||
{t('settingsWindow.import')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,20 +646,17 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
<span className="settings-breadcrumb-sub">{subCategoryTitle}</span>
|
||||
</div>
|
||||
{selectedCategory?.description && (
|
||||
<p className="settings-category-desc">{selectedCategory.description}</p>
|
||||
<p className="settings-category-desc">{resolveText(selectedCategory.description)}</p>
|
||||
)}
|
||||
<div className="settings-category-actions">
|
||||
<button className="settings-category-action-btn" onClick={handleResetToDefault}>
|
||||
设置为默认值
|
||||
{t('settingsWindow.resetToDefault')}
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleExport}>
|
||||
导出......
|
||||
{t('settingsWindow.export')}
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleImport}>
|
||||
导入......
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleResetToDefault}>
|
||||
重置为默认
|
||||
{t('settingsWindow.import')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -652,7 +678,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
) : (
|
||||
<ChevronRight size={12} />
|
||||
)}
|
||||
<span>{section.title}</span>
|
||||
<span>{resolveText(section.title)}</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
@@ -671,7 +697,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{!selectedCategory && (
|
||||
<div className="settings-empty-new">
|
||||
<SettingsIcon size={48} />
|
||||
<p>请选择一个设置分类</p>
|
||||
<p>{t('settingsWindow.selectCategory')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,9 @@ import { Globe, ChevronDown, Download, X, Loader2, Trash2, CheckCircle, AlertCir
|
||||
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
||||
import { StartupLogo } from './StartupLogo';
|
||||
import { TauriAPI, type EnvironmentCheckResult } from '../api/tauri';
|
||||
import { useLocale, type Locale } from '../hooks/useLocale';
|
||||
import '../styles/StartupPage.css';
|
||||
|
||||
type Locale = 'en' | 'zh';
|
||||
|
||||
interface StartupPageProps {
|
||||
onOpenProject: () => void;
|
||||
onCreateProject: () => void;
|
||||
@@ -16,7 +15,6 @@ interface StartupPageProps {
|
||||
onDeleteProject?: (projectPath: string) => Promise<void>;
|
||||
onLocaleChange?: (locale: Locale) => void;
|
||||
recentProjects?: string[];
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const LANGUAGES = [
|
||||
@@ -24,7 +22,8 @@ const LANGUAGES = [
|
||||
{ code: 'zh', name: '中文' }
|
||||
];
|
||||
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [] }: StartupPageProps) {
|
||||
const { t, locale } = useLocale();
|
||||
const [showLogo, setShowLogo] = useState(true);
|
||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
@@ -80,53 +79,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
});
|
||||
}, []);
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'ESEngine Editor',
|
||||
subtitle: 'Professional Game Development Tool',
|
||||
openProject: 'Open Project',
|
||||
createProject: 'Create Project',
|
||||
recentProjects: 'Recent Projects',
|
||||
noRecentProjects: 'No recent projects',
|
||||
updateAvailable: 'New version available',
|
||||
updateNow: 'Update Now',
|
||||
installing: 'Installing...',
|
||||
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',
|
||||
envReady: 'Environment Ready',
|
||||
envNotReady: 'Environment Issue',
|
||||
esbuildReady: 'esbuild ready',
|
||||
esbuildMissing: 'esbuild not found'
|
||||
},
|
||||
zh: {
|
||||
title: 'ESEngine 编辑器',
|
||||
subtitle: '专业游戏开发工具',
|
||||
openProject: '打开项目',
|
||||
createProject: '创建新项目',
|
||||
recentProjects: '最近的项目',
|
||||
noRecentProjects: '没有最近的项目',
|
||||
updateAvailable: '发现新版本',
|
||||
updateNow: '立即更新',
|
||||
installing: '正在安装...',
|
||||
later: '稍后',
|
||||
removeFromList: '从列表中移除',
|
||||
deleteProject: '删除项目',
|
||||
deleteConfirmTitle: '删除项目',
|
||||
deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
envReady: '环境就绪',
|
||||
envNotReady: '环境问题',
|
||||
esbuildReady: 'esbuild 就绪',
|
||||
esbuildMissing: '未找到 esbuild'
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallUpdate = async () => {
|
||||
setIsInstalling(true);
|
||||
const success = await installUpdate();
|
||||
@@ -136,8 +88,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
// 如果成功,应用会重启,不需要处理
|
||||
};
|
||||
|
||||
const t = translations[locale as keyof typeof translations] || translations.en;
|
||||
const versionText = locale === 'zh' ? `版本 ${appVersion}` : `Version ${appVersion}`;
|
||||
const versionText = `${t('startup.version')} ${appVersion}`;
|
||||
|
||||
const handleLogoComplete = () => {
|
||||
setShowLogo(false);
|
||||
@@ -147,8 +98,8 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="startup-page">
|
||||
{showLogo && <StartupLogo onAnimationComplete={handleLogoComplete} />}
|
||||
<div className="startup-header">
|
||||
<h1 className="startup-title">{t.title}</h1>
|
||||
<p className="startup-subtitle">{t.subtitle}</p>
|
||||
<h1 className="startup-title">{t('startup.title')}</h1>
|
||||
<p className="startup-subtitle">{t('startup.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="startup-content">
|
||||
@@ -157,21 +108,21 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
||||
</svg>
|
||||
<span>{t.openProject}</span>
|
||||
<span>{t('startup.openProject')}</span>
|
||||
</button>
|
||||
|
||||
<button className="startup-action-btn" onClick={onCreateProject}>
|
||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>{t.createProject}</span>
|
||||
<span>{t('startup.createProject')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="startup-recent">
|
||||
<h2 className="recent-title">{t.recentProjects}</h2>
|
||||
<h2 className="recent-title">{t('startup.recentProjects')}</h2>
|
||||
{recentProjects.length === 0 ? (
|
||||
<p className="recent-empty">{t.noRecentProjects}</p>
|
||||
<p className="recent-empty">{t('startup.noRecentProjects')}</p>
|
||||
) : (
|
||||
<ul className="recent-list">
|
||||
{recentProjects.map((project, index) => (
|
||||
@@ -201,7 +152,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
e.stopPropagation();
|
||||
onRemoveRecentProject(project);
|
||||
}}
|
||||
title={t.removeFromList}
|
||||
title={t('startup.removeFromList')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
@@ -219,7 +170,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="update-banner-content">
|
||||
<Download size={16} />
|
||||
<span className="update-banner-text">
|
||||
{t.updateAvailable}: v{updateInfo.version}
|
||||
{t('startup.updateAvailable')}: v{updateInfo.version}
|
||||
</span>
|
||||
<button
|
||||
className="update-banner-btn primary"
|
||||
@@ -229,17 +180,17 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
{t.installing}
|
||||
{t('startup.installing')}
|
||||
</>
|
||||
) : (
|
||||
t.updateNow
|
||||
t('startup.updateNow')
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="update-banner-close"
|
||||
onClick={() => setShowUpdateBanner(false)}
|
||||
disabled={isInstalling}
|
||||
title={t.later}
|
||||
title={t('startup.later')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -255,7 +206,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div
|
||||
className={`startup-env-status ${envCheck.ready ? 'ready' : 'warning'}`}
|
||||
onClick={() => setShowEnvStatus(!showEnvStatus)}
|
||||
title={envCheck.ready ? t.envReady : t.envNotReady}
|
||||
title={envCheck.ready ? t('startup.envReady') : t('startup.envNotReady')}
|
||||
>
|
||||
{envCheck.ready ? (
|
||||
<CheckCircle size={14} />
|
||||
@@ -265,7 +216,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
{showEnvStatus && (
|
||||
<div className="startup-env-tooltip">
|
||||
<div className="env-tooltip-title">
|
||||
{envCheck.ready ? t.envReady : t.envNotReady}
|
||||
{envCheck.ready ? t('startup.envReady') : t('startup.envNotReady')}
|
||||
</div>
|
||||
<div className={`env-tooltip-item ${envCheck.esbuild.available ? 'ok' : 'error'}`}>
|
||||
{envCheck.esbuild.available ? (
|
||||
@@ -277,7 +228,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle size={12} />
|
||||
<span>{t.esbuildMissing}</span>
|
||||
<span>{t('startup.esbuildMissing')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -335,7 +286,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
<span>{t.removeFromList}</span>
|
||||
<span>{t('startup.removeFromList')}</span>
|
||||
</button>
|
||||
{onDeleteProject && (
|
||||
<button
|
||||
@@ -346,7 +297,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>{t.deleteProject}</span>
|
||||
<span>{t('startup.deleteProject')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -359,10 +310,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="startup-dialog">
|
||||
<div className="startup-dialog-header">
|
||||
<Trash2 size={20} className="dialog-icon-danger" />
|
||||
<h3>{t.deleteConfirmTitle}</h3>
|
||||
<h3>{t('startup.deleteConfirmTitle')}</h3>
|
||||
</div>
|
||||
<div className="startup-dialog-body">
|
||||
<p>{t.deleteConfirmMessage}</p>
|
||||
<p>{t('startup.deleteConfirmMessage')}</p>
|
||||
<p className="startup-dialog-path">{deleteConfirm}</p>
|
||||
</div>
|
||||
<div className="startup-dialog-footer">
|
||||
@@ -370,7 +321,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
className="startup-dialog-btn"
|
||||
onClick={() => setDeleteConfirm(null)}
|
||||
>
|
||||
{t.cancel}
|
||||
{t('startup.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="startup-dialog-btn danger"
|
||||
@@ -386,7 +337,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
>
|
||||
{t.delete}
|
||||
{t('startup.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FolderOpen, FileText, Terminal, ChevronDown, ChevronUp, Activity, Wifi,
|
||||
import type { MessageHub, LogService } from '@esengine/editor-core';
|
||||
import { ContentBrowser } from './ContentBrowser';
|
||||
import { OutputLogPanel } from './OutputLogPanel';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/StatusBar.css';
|
||||
|
||||
interface StatusBarProps {
|
||||
@@ -26,6 +27,7 @@ export function StatusBar({
|
||||
projectPath,
|
||||
onOpenScene
|
||||
}: StatusBarProps) {
|
||||
const { t } = useLocale();
|
||||
const [consoleInput, setConsoleInput] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>('output');
|
||||
const [contentDrawerOpen, setContentDrawerOpen] = useState(false);
|
||||
@@ -254,7 +256,7 @@ export function StatusBar({
|
||||
onClick={handleContentDrawerClick}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
<span>{locale === 'zh' ? '内容侧滑菜单' : 'Content Drawer'}</span>
|
||||
<span>{t('statusBar.contentDrawer')}</span>
|
||||
{contentDrawerOpen ? <ChevronDown size={12} /> : <ChevronUp size={12} />}
|
||||
</button>
|
||||
|
||||
@@ -265,7 +267,7 @@ export function StatusBar({
|
||||
onClick={handleOutputLogClick}
|
||||
>
|
||||
<FileText size={12} />
|
||||
<span>{locale === 'zh' ? '输出日志' : 'Output Log'}</span>
|
||||
<span>{t('statusBar.outputLog')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -282,7 +284,7 @@ export function StatusBar({
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder={locale === 'zh' ? '输入控制台命令' : 'Enter Console Command'}
|
||||
placeholder={t('statusBar.consolePlaceholder')}
|
||||
value={consoleInput}
|
||||
onChange={(e) => setConsoleInput(e.target.value)}
|
||||
onKeyDown={handleConsoleSubmit}
|
||||
@@ -294,17 +296,17 @@ export function StatusBar({
|
||||
<div className="status-bar-right">
|
||||
<button className="status-bar-indicator">
|
||||
<Activity size={12} />
|
||||
<span>{locale === 'zh' ? '回追踪' : 'Trace'}</span>
|
||||
<span>{t('statusBar.trace')}</span>
|
||||
<ChevronDown size={10} />
|
||||
</button>
|
||||
|
||||
<div className="status-bar-divider" />
|
||||
|
||||
<div className="status-bar-icon-group">
|
||||
<button className="status-bar-icon-btn" title={locale === 'zh' ? '网络' : 'Network'}>
|
||||
<button className="status-bar-icon-btn" title={t('statusBar.network')}>
|
||||
<Wifi size={14} />
|
||||
</button>
|
||||
<button className="status-bar-icon-btn" title={locale === 'zh' ? '源代码管理' : 'Source Control'}>
|
||||
<button className="status-bar-icon-btn" title={t('statusBar.sourceControl')}>
|
||||
<GitBranch size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -313,11 +315,11 @@ export function StatusBar({
|
||||
|
||||
<div className="status-bar-info">
|
||||
<Save size={12} />
|
||||
<span>{locale === 'zh' ? '所有已保存' : 'All Saved'}</span>
|
||||
<span>{t('statusBar.allSaved')}</span>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-info">
|
||||
<span>{locale === 'zh' ? '版本控制' : 'Revision Control'}</span>
|
||||
<span>{t('statusBar.revisionControl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { UIRegistry, MessageHub, PluginManager } from '@esengine/editor-core';
|
||||
import type { MenuItem as PluginMenuItem } from '@esengine/editor-core';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/TitleBar.css';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -17,7 +18,6 @@ interface MenuItem {
|
||||
|
||||
interface TitleBarProps {
|
||||
projectName?: string;
|
||||
locale?: string;
|
||||
uiRegistry?: UIRegistry;
|
||||
messageHub?: MessageHub;
|
||||
pluginManager?: PluginManager;
|
||||
@@ -41,7 +41,6 @@ interface TitleBarProps {
|
||||
|
||||
export function TitleBar({
|
||||
projectName = 'Untitled',
|
||||
locale = 'en',
|
||||
uiRegistry,
|
||||
messageHub,
|
||||
pluginManager,
|
||||
@@ -62,6 +61,7 @@ export function TitleBar({
|
||||
onReloadPlugins,
|
||||
onOpenBuildSettings
|
||||
}: TitleBarProps) {
|
||||
const { t } = useLocale();
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
@@ -119,109 +119,31 @@ export function TitleBar({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
file: 'File',
|
||||
newScene: 'New Scene',
|
||||
openScene: 'Open Scene',
|
||||
saveScene: 'Save Scene',
|
||||
saveSceneAs: 'Save Scene As...',
|
||||
openProject: 'Open Project',
|
||||
closeProject: 'Close Project',
|
||||
exit: 'Exit',
|
||||
edit: 'Edit',
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
cut: 'Cut',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
delete: 'Delete',
|
||||
selectAll: 'Select All',
|
||||
window: 'Window',
|
||||
sceneHierarchy: 'Scene Hierarchy',
|
||||
inspector: 'Inspector',
|
||||
assets: 'Assets',
|
||||
console: 'Console',
|
||||
viewport: 'Viewport',
|
||||
pluginManager: 'Plugin Manager',
|
||||
tools: 'Tools',
|
||||
createPlugin: 'Create Plugin',
|
||||
reloadPlugins: 'Reload Plugins',
|
||||
portManager: 'Port Manager',
|
||||
settings: 'Settings',
|
||||
help: 'Help',
|
||||
documentation: 'Documentation',
|
||||
checkForUpdates: 'Check for Updates',
|
||||
about: 'About',
|
||||
devtools: 'Developer Tools',
|
||||
buildSettings: 'Build Settings'
|
||||
},
|
||||
zh: {
|
||||
file: '文件',
|
||||
newScene: '新建场景',
|
||||
openScene: '打开场景',
|
||||
saveScene: '保存场景',
|
||||
saveSceneAs: '场景另存为...',
|
||||
openProject: '打开项目',
|
||||
closeProject: '关闭项目',
|
||||
exit: '退出',
|
||||
edit: '编辑',
|
||||
undo: '撤销',
|
||||
redo: '重做',
|
||||
cut: '剪切',
|
||||
copy: '复制',
|
||||
paste: '粘贴',
|
||||
delete: '删除',
|
||||
selectAll: '全选',
|
||||
window: '窗口',
|
||||
sceneHierarchy: '场景层级',
|
||||
inspector: '检视器',
|
||||
assets: '资产',
|
||||
console: '控制台',
|
||||
viewport: '视口',
|
||||
pluginManager: '插件管理器',
|
||||
tools: '工具',
|
||||
createPlugin: '创建插件',
|
||||
reloadPlugins: '重新加载插件',
|
||||
portManager: '端口管理器',
|
||||
settings: '设置',
|
||||
help: '帮助',
|
||||
documentation: '文档',
|
||||
checkForUpdates: '检查更新',
|
||||
about: '关于',
|
||||
devtools: '开发者工具',
|
||||
buildSettings: '构建设置'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
const menus: Record<string, MenuItem[]> = {
|
||||
file: [
|
||||
{ label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ label: t('menu.file.newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('menu.file.openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ separator: true },
|
||||
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ label: t('menu.file.saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('menu.file.saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ separator: true },
|
||||
{ label: t('buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ label: t('menu.file.buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ separator: true },
|
||||
{ label: t('openProject'), onClick: onOpenProject },
|
||||
{ label: t('closeProject'), onClick: onCloseProject },
|
||||
{ label: t('menu.file.openProject'), onClick: onOpenProject },
|
||||
{ label: t('menu.file.closeProject'), onClick: onCloseProject },
|
||||
{ separator: true },
|
||||
{ label: t('exit'), onClick: onExit }
|
||||
{ label: t('menu.file.exit'), onClick: onExit }
|
||||
],
|
||||
edit: [
|
||||
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ label: t('menu.edit.undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('menu.edit.redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('delete'), shortcut: 'Delete', disabled: true },
|
||||
{ label: t('menu.edit.cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('menu.edit.copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('menu.edit.paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('menu.edit.delete'), shortcut: 'Delete', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
{ label: t('menu.edit.selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
],
|
||||
window: [
|
||||
...pluginMenuItems.map((item) => ({
|
||||
@@ -231,25 +153,34 @@ export function TitleBar({
|
||||
onClick: item.onClick
|
||||
})),
|
||||
...(pluginMenuItems.length > 0 ? [{ separator: true } as MenuItem] : []),
|
||||
{ label: t('pluginManager'), onClick: onOpenPluginManager },
|
||||
{ label: t('menu.window.pluginManager'), onClick: onOpenPluginManager },
|
||||
{ separator: true },
|
||||
{ label: t('devtools'), onClick: onToggleDevtools }
|
||||
{ label: t('menu.window.devtools'), onClick: onToggleDevtools }
|
||||
],
|
||||
tools: [
|
||||
{ label: t('createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ label: t('menu.tools.createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('menu.tools.reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ separator: true },
|
||||
{ label: t('portManager'), onClick: onOpenPortManager },
|
||||
{ label: t('menu.tools.portManager'), onClick: onOpenPortManager },
|
||||
{ separator: true },
|
||||
{ label: t('settings'), onClick: onOpenSettings }
|
||||
{ label: t('menu.tools.settings'), onClick: onOpenSettings }
|
||||
],
|
||||
help: [
|
||||
{ label: t('documentation'), disabled: true },
|
||||
{ label: t('menu.help.documentation'), disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('about'), onClick: onOpenAbout }
|
||||
{ label: t('menu.help.about'), onClick: onOpenAbout }
|
||||
]
|
||||
};
|
||||
|
||||
// 菜单键到翻译键的映射 | Map menu keys to translation keys
|
||||
const menuTitleKeys: Record<string, string> = {
|
||||
file: 'menu.file.title',
|
||||
edit: 'menu.edit.title',
|
||||
window: 'menu.window.title',
|
||||
tools: 'menu.tools.title',
|
||||
help: 'menu.help.title'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
@@ -300,7 +231,7 @@ export function TitleBar({
|
||||
className={`titlebar-menu-button ${openMenu === menuKey ? 'active' : ''}`}
|
||||
onClick={() => handleMenuClick(menuKey)}
|
||||
>
|
||||
{t(menuKey)}
|
||||
{t(menuTitleKeys[menuKey] || menuKey)}
|
||||
</button>
|
||||
{openMenu === menuKey && menus[menuKey] && (
|
||||
<div className="titlebar-dropdown">
|
||||
@@ -338,12 +269,12 @@ export function TitleBar({
|
||||
<div className="titlebar-right">
|
||||
<span className="titlebar-project-name" data-tauri-drag-region>{projectName}</span>
|
||||
<div className="titlebar-window-controls">
|
||||
<button className="titlebar-button" onClick={handleMinimize} title="Minimize">
|
||||
<button className="titlebar-button" onClick={handleMinimize} title={t('titleBar.minimize')}>
|
||||
<svg width="10" height="1" viewBox="0 0 10 1">
|
||||
<rect width="10" height="1" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="titlebar-button" onClick={handleMaximize} title={isMaximized ? "Restore" : "Maximize"}>
|
||||
<button className="titlebar-button" onClick={handleMaximize} title={isMaximized ? t('titleBar.restore') : t('titleBar.maximize')}>
|
||||
{isMaximized ? (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M2 0v2H0v8h8V8h2V0H2zm6 8H2V4h6v4z" fill="currentColor"/>
|
||||
@@ -354,7 +285,7 @@ export function TitleBar({
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<button className="titlebar-button titlebar-button-close" onClick={handleClose} title="Close">
|
||||
<button className="titlebar-button titlebar-button-close" onClick={handleClose} title={t('titleBar.close')}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import '../styles/Viewport.css';
|
||||
import { useEngine } from '../hooks/useEngine';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { EngineService } from '../services/EngineService';
|
||||
import { Core, Entity, SceneSerializer } from '@esengine/ecs-framework';
|
||||
import { MessageHub, ProjectService, AssetRegistryService } from '@esengine/editor-core';
|
||||
@@ -207,6 +208,7 @@ interface ViewportProps {
|
||||
}
|
||||
|
||||
export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
const { t } = useLocale();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [playState, setPlayState] = useState<PlayState>('stopped');
|
||||
@@ -704,9 +706,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
// Check if there's a camera entity
|
||||
const cameraEntity = findPlayerCamera();
|
||||
if (!cameraEntity) {
|
||||
const warningMessage = locale === 'zh'
|
||||
? '缺少相机: 场景中没有相机实体,请添加一个带有Camera组件的实体'
|
||||
: 'Missing Camera: No camera entity in scene. Please add an entity with Camera component.';
|
||||
const warningMessage = t('viewport.errors.missingCamera');
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:show', {
|
||||
message: warningMessage,
|
||||
@@ -781,8 +781,8 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
const scene = engineService.getScene();
|
||||
if (!scene) {
|
||||
messageHub?.publish('notification:error', {
|
||||
title: locale === 'zh' ? '错误' : 'Error',
|
||||
message: locale === 'zh' ? '没有可运行的场景' : 'No scene to run'
|
||||
title: t('common.error'),
|
||||
message: t('viewport.errors.noScene')
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1008,13 +1008,13 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
await open(serverUrl);
|
||||
|
||||
messageHub?.publish('notification:success', {
|
||||
title: locale === 'zh' ? '浏览器运行' : 'Run in Browser',
|
||||
message: locale === 'zh' ? `已在浏览器中打开: ${serverUrl}` : `Opened in browser: ${serverUrl}`
|
||||
title: t('viewport.run.inBrowser'),
|
||||
message: t('viewport.run.openedInBrowser', { url: serverUrl })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to run in browser:', error);
|
||||
messageHub?.publish('notification:error', {
|
||||
title: locale === 'zh' ? '运行失败' : 'Run Failed',
|
||||
title: t('viewport.run.failed'),
|
||||
message: String(error)
|
||||
});
|
||||
}
|
||||
@@ -1026,8 +1026,8 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
if (!Core.scene) {
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:warning', {
|
||||
title: locale === 'zh' ? '无场景' : 'No Scene',
|
||||
message: locale === 'zh' ? '请先创建场景' : 'Please create a scene first'
|
||||
title: t('viewport.notifications.noScene'),
|
||||
message: t('viewport.errors.noSceneFirst')
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -1140,15 +1140,15 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:success', {
|
||||
title: locale === 'zh' ? '服务器已启动' : 'Server Started',
|
||||
message: locale === 'zh' ? `预览地址: ${previewUrl}` : `Preview URL: ${previewUrl}`
|
||||
title: t('viewport.run.serverStarted'),
|
||||
message: t('viewport.run.previewUrl', { url: previewUrl })
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to run on device:', error);
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:error', {
|
||||
title: locale === 'zh' ? '启动失败' : 'Failed to Start',
|
||||
title: t('viewport.run.startFailed'),
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
@@ -1281,28 +1281,28 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'select' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('select')}
|
||||
title={locale === 'zh' ? '选择 (Q)' : 'Select (Q)'}
|
||||
title={t('viewport.tools.select')}
|
||||
>
|
||||
<MousePointer2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'move' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('move')}
|
||||
title={locale === 'zh' ? '移动 (W)' : 'Move (W)'}
|
||||
title={t('viewport.tools.move')}
|
||||
>
|
||||
<Move size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'rotate' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('rotate')}
|
||||
title={locale === 'zh' ? '旋转 (E)' : 'Rotate (E)'}
|
||||
title={t('viewport.tools.rotate')}
|
||||
>
|
||||
<RotateCw size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'scale' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('scale')}
|
||||
title={locale === 'zh' ? '缩放 (R)' : 'Scale (R)'}
|
||||
title={t('viewport.tools.scale')}
|
||||
>
|
||||
<Scaling size={14} />
|
||||
</button>
|
||||
@@ -1314,7 +1314,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${snapEnabled ? 'active' : ''}`}
|
||||
onClick={() => setSnapEnabled(!snapEnabled)}
|
||||
title={locale === 'zh' ? '吸附开关' : 'Toggle Snap'}
|
||||
title={t('viewport.snap.toggle')}
|
||||
>
|
||||
<Magnet size={14} />
|
||||
</button>
|
||||
@@ -1324,7 +1324,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowGridSnapMenu(!showGridSnapMenu); }}
|
||||
title={locale === 'zh' ? '网格吸附' : 'Grid Snap'}
|
||||
title={t('viewport.snap.grid')}
|
||||
>
|
||||
<Grid3x3 size={12} />
|
||||
<span>{gridSnapValue}</span>
|
||||
@@ -1350,7 +1350,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowRotationSnapMenu(!showRotationSnapMenu); }}
|
||||
title={locale === 'zh' ? '旋转吸附' : 'Rotation Snap'}
|
||||
title={t('viewport.snap.rotation')}
|
||||
>
|
||||
<RotateCw size={12} />
|
||||
<span>{rotationSnapValue}°</span>
|
||||
@@ -1376,7 +1376,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowScaleSnapMenu(!showScaleSnapMenu); }}
|
||||
title={locale === 'zh' ? '缩放吸附' : 'Scale Snap'}
|
||||
title={t('viewport.snap.scale')}
|
||||
>
|
||||
<Scaling size={12} />
|
||||
<span>{scaleSnapValue}</span>
|
||||
@@ -1403,14 +1403,14 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${showGrid ? 'active' : ''}`}
|
||||
onClick={() => setShowGrid(!showGrid)}
|
||||
title={locale === 'zh' ? '显示网格' : 'Show Grid'}
|
||||
title={t('viewport.view.showGrid')}
|
||||
>
|
||||
<Grid3x3 size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${showGizmos ? 'active' : ''}`}
|
||||
onClick={() => setShowGizmos(!showGizmos)}
|
||||
title={locale === 'zh' ? '显示辅助线' : 'Show Gizmos'}
|
||||
title={t('viewport.view.showGizmos')}
|
||||
>
|
||||
{showGizmos ? <Eye size={14} /> : <EyeOff size={14} />}
|
||||
</button>
|
||||
@@ -1429,7 +1429,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${showStats ? 'active' : ''}`}
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
title={locale === 'zh' ? '统计信息' : 'Stats'}
|
||||
title={t('viewport.view.stats')}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
@@ -1438,7 +1438,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-btn"
|
||||
onClick={handleReset}
|
||||
title={locale === 'zh' ? '重置视图' : 'Reset View'}
|
||||
title={t('viewport.view.resetView')}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
@@ -1447,7 +1447,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-btn"
|
||||
onClick={handleFullscreen}
|
||||
title={locale === 'zh' ? '全屏' : 'Fullscreen'}
|
||||
title={t('viewport.view.fullscreen')}
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
@@ -1459,7 +1459,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowRunMenu(!showRunMenu); }}
|
||||
title={locale === 'zh' ? '运行选项' : 'Run Options'}
|
||||
title={t('viewport.run.options')}
|
||||
>
|
||||
<Globe size={14} />
|
||||
<ChevronDown size={10} />
|
||||
@@ -1468,11 +1468,11 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<div className="viewport-snap-menu viewport-snap-menu-right">
|
||||
<button onClick={handleRunInBrowser}>
|
||||
<Globe size={14} />
|
||||
{locale === 'zh' ? '浏览器运行' : 'Run in Browser'}
|
||||
{t('viewport.run.inBrowser')}
|
||||
</button>
|
||||
<button onClick={handleRunOnDevice}>
|
||||
<QrCode size={14} />
|
||||
{locale === 'zh' ? '真机运行' : 'Run on Device'}
|
||||
{t('viewport.run.onDevice')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
*/
|
||||
import { AlertCircle, CheckCircle, ExternalLink, Github, Loader } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { useForumAuth } from '../../hooks/useForum';
|
||||
import './ForumAuth.css';
|
||||
|
||||
type AuthStatus = 'idle' | 'pending' | 'authorized' | 'error';
|
||||
|
||||
export function ForumAuth() {
|
||||
const { i18n } = useTranslation();
|
||||
const { t } = useLocale();
|
||||
const { requestDeviceCode, authenticateWithDeviceFlow, signInWithGitHubToken } = useForumAuth();
|
||||
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>('idle');
|
||||
@@ -20,8 +20,6 @@ export function ForumAuth() {
|
||||
const [verificationUri, setVerificationUri] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const isEnglish = i18n.language === 'en';
|
||||
|
||||
const handleGitHubLogin = async () => {
|
||||
setAuthStatus('pending');
|
||||
setError(null);
|
||||
@@ -60,7 +58,7 @@ export function ForumAuth() {
|
||||
} catch (err) {
|
||||
console.error('[ForumAuth] GitHub login failed:', err);
|
||||
setAuthStatus('error');
|
||||
setError(err instanceof Error ? err.message : (isEnglish ? 'Authorization failed' : '授权失败'));
|
||||
setError(err instanceof Error ? err.message : t('forum.authFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,21 +82,21 @@ export function ForumAuth() {
|
||||
<div className="forum-auth-card">
|
||||
<div className="forum-auth-header">
|
||||
<Github size={32} className="forum-auth-icon" />
|
||||
<h2>{isEnglish ? 'ESEngine Community' : 'ESEngine 社区'}</h2>
|
||||
<p>{isEnglish ? 'Sign in with GitHub to join the discussion' : '使用 GitHub 登录参与讨论'}</p>
|
||||
<h2>{t('forum.communityTitle')}</h2>
|
||||
<p>{t('forum.signInWithGitHub')}</p>
|
||||
</div>
|
||||
|
||||
{/* 初始状态 | Idle state */}
|
||||
{authStatus === 'idle' && (
|
||||
<div className="forum-auth-content">
|
||||
<div className="forum-auth-instructions">
|
||||
<p>{isEnglish ? '1. Click the button below' : '1. 点击下方按钮'}</p>
|
||||
<p>{isEnglish ? '2. Enter the code on GitHub' : '2. 在 GitHub 页面输入验证码'}</p>
|
||||
<p>{isEnglish ? '3. Authorize the application' : '3. 授权应用'}</p>
|
||||
<p>{t('forum.step1')}</p>
|
||||
<p>{t('forum.step2')}</p>
|
||||
<p>{t('forum.step3')}</p>
|
||||
</div>
|
||||
<button className="forum-auth-github-btn" onClick={handleGitHubLogin}>
|
||||
<Github size={16} />
|
||||
<span>{isEnglish ? 'Continue with GitHub' : '使用 GitHub 登录'}</span>
|
||||
<span>{t('forum.continueWithGitHub')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -108,18 +106,18 @@ export function ForumAuth() {
|
||||
<div className="forum-auth-pending">
|
||||
<Loader size={24} className="spinning" />
|
||||
<p className="forum-auth-pending-text">
|
||||
{isEnglish ? 'Waiting for authorization...' : '等待授权中...'}
|
||||
{t('forum.waitingForAuth')}
|
||||
</p>
|
||||
|
||||
{userCode && (
|
||||
<div className="forum-auth-code-section">
|
||||
<label>{isEnglish ? 'Enter this code on GitHub:' : '在 GitHub 输入此验证码:'}</label>
|
||||
<label>{t('forum.enterCodeOnGitHub')}</label>
|
||||
<div className="forum-auth-code-box">
|
||||
<span className="forum-auth-code">{userCode}</span>
|
||||
<button
|
||||
className="forum-auth-copy-btn"
|
||||
onClick={() => copyToClipboard(userCode)}
|
||||
title={isEnglish ? 'Copy code' : '复制验证码'}
|
||||
title={t('forum.copyCode')}
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
@@ -129,7 +127,7 @@ export function ForumAuth() {
|
||||
onClick={() => open(verificationUri)}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
<span>{isEnglish ? 'Open GitHub' : '打开 GitHub'}</span>
|
||||
<span>{t('forum.openGitHub')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -140,7 +138,7 @@ export function ForumAuth() {
|
||||
{authStatus === 'authorized' && (
|
||||
<div className="forum-auth-success">
|
||||
<CheckCircle size={32} className="forum-auth-success-icon" />
|
||||
<p>{isEnglish ? 'Authorization successful!' : '授权成功!'}</p>
|
||||
<p>{t('forum.authSuccess')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -148,10 +146,10 @@ export function ForumAuth() {
|
||||
{authStatus === 'error' && (
|
||||
<div className="forum-auth-error-state">
|
||||
<AlertCircle size={32} className="forum-auth-error-icon" />
|
||||
<p>{isEnglish ? 'Authorization failed' : '授权失败'}</p>
|
||||
<p>{t('forum.authFailed')}</p>
|
||||
{error && <p className="forum-auth-error-detail">{error}</p>}
|
||||
<button className="forum-auth-retry-btn" onClick={handleRetry}>
|
||||
{isEnglish ? 'Try Again' : '重试'}
|
||||
{t('forum.tryAgain')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { getForumService } from '../../services/forum';
|
||||
import type { Category } from '../../services/forum';
|
||||
import { parseEmoji } from './utils';
|
||||
@@ -17,14 +18,14 @@ import './ForumCreatePost.css';
|
||||
|
||||
interface ForumCreatePostProps {
|
||||
categories: Category[];
|
||||
isEnglish: boolean;
|
||||
onBack: () => void;
|
||||
onCreated: () => void;
|
||||
}
|
||||
|
||||
type EditorTab = 'write' | 'preview';
|
||||
|
||||
export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: ForumCreatePostProps) {
|
||||
export function ForumCreatePost({ categories, onBack, onCreated }: ForumCreatePostProps) {
|
||||
const { t } = useLocale();
|
||||
const [title, setTitle] = useState('');
|
||||
const [body, setBody] = useState('');
|
||||
const [categoryId, setCategoryId] = useState<string | null>(null);
|
||||
@@ -76,12 +77,12 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ForumCreatePost] Upload failed:', err);
|
||||
setError(err instanceof Error ? err.message : (isEnglish ? 'Failed to upload image' : '图片上传失败'));
|
||||
setError(err instanceof Error ? err.message : t('forum.failedToUploadImage'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
}, [body, forumService, isEnglish, uploading]);
|
||||
}, [body, forumService, t, uploading]);
|
||||
|
||||
/**
|
||||
* 处理拖拽事件
|
||||
@@ -147,15 +148,15 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
|
||||
// 验证 | Validation
|
||||
if (!title.trim()) {
|
||||
setError(isEnglish ? 'Please enter a title' : '请输入标题');
|
||||
setError(t('forum.enterTitle'));
|
||||
return;
|
||||
}
|
||||
if (!body.trim()) {
|
||||
setError(isEnglish ? 'Please enter content' : '请输入内容');
|
||||
setError(t('forum.enterContent'));
|
||||
return;
|
||||
}
|
||||
if (!categoryId) {
|
||||
setError(isEnglish ? 'Please select a category' : '请选择分类');
|
||||
setError(t('forum.selectCategoryError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,11 +171,11 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
if (post) {
|
||||
onCreated();
|
||||
} else {
|
||||
setError(isEnglish ? 'Failed to create discussion' : '创建讨论失败,请稍后重试');
|
||||
setError(t('forum.failedToCreateDiscussion'));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ForumCreatePost] Error:', err);
|
||||
setError(err instanceof Error ? err.message : (isEnglish ? 'An error occurred' : '发生错误,请稍后重试'));
|
||||
setError(err instanceof Error ? err.message : t('forum.anErrorOccurred'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
@@ -201,13 +202,13 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
};
|
||||
|
||||
const toolbarButtons = [
|
||||
{ icon: <Bold size={14} />, action: () => insertMarkdown('**', '**', 'bold'), title: isEnglish ? 'Bold' : '粗体' },
|
||||
{ icon: <Italic size={14} />, action: () => insertMarkdown('*', '*', 'italic'), title: isEnglish ? 'Italic' : '斜体' },
|
||||
{ icon: <Code size={14} />, action: () => insertMarkdown('`', '`', 'code'), title: isEnglish ? 'Inline code' : '行内代码' },
|
||||
{ icon: <Link size={14} />, action: () => insertMarkdown('[', '](url)', 'link text'), title: isEnglish ? 'Link' : '链接' },
|
||||
{ icon: <List size={14} />, action: () => insertMarkdown('\n- ', '', 'list item'), title: isEnglish ? 'List' : '列表' },
|
||||
{ icon: <Quote size={14} />, action: () => insertMarkdown('\n> ', '', 'quote'), title: isEnglish ? 'Quote' : '引用' },
|
||||
{ icon: <Upload size={14} />, action: () => fileInputRef.current?.click(), title: isEnglish ? 'Upload image' : '上传图片' },
|
||||
{ icon: <Bold size={14} />, action: () => insertMarkdown('**', '**', 'bold'), title: t('forum.bold') },
|
||||
{ icon: <Italic size={14} />, action: () => insertMarkdown('*', '*', 'italic'), title: t('forum.italic') },
|
||||
{ icon: <Code size={14} />, action: () => insertMarkdown('`', '`', 'code'), title: t('forum.inlineCode') },
|
||||
{ icon: <Link size={14} />, action: () => insertMarkdown('[', '](url)', 'link text'), title: t('forum.link') },
|
||||
{ icon: <List size={14} />, action: () => insertMarkdown('\n- ', '', 'list item'), title: t('forum.list') },
|
||||
{ icon: <Quote size={14} />, action: () => insertMarkdown('\n> ', '', 'quote'), title: t('forum.quote') },
|
||||
{ icon: <Upload size={14} />, action: () => fileInputRef.current?.click(), title: t('forum.uploadImage') },
|
||||
];
|
||||
|
||||
const selectedCategory = categories.find(c => c.id === categoryId);
|
||||
@@ -217,14 +218,14 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
{/* 返回按钮 | Back button */}
|
||||
<button className="forum-back-btn" onClick={onBack}>
|
||||
<ArrowLeft size={18} />
|
||||
<span>{isEnglish ? 'Back to list' : '返回列表'}</span>
|
||||
<span>{t('forum.backToList')}</span>
|
||||
</button>
|
||||
|
||||
<div className="forum-create-container">
|
||||
{/* 左侧:编辑区 | Left: Editor */}
|
||||
<div className="forum-create-main">
|
||||
<div className="forum-create-header">
|
||||
<h2>{isEnglish ? 'Start a Discussion' : '发起讨论'}</h2>
|
||||
<h2>{t('forum.startDiscussion')}</h2>
|
||||
{selectedCategory && (
|
||||
<span className="forum-create-selected-category">
|
||||
{parseEmoji(selectedCategory.emoji)} {selectedCategory.name}
|
||||
@@ -235,7 +236,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
<form className="forum-create-form" onSubmit={handleSubmit}>
|
||||
{/* 分类选择 | Category selection */}
|
||||
<div className="forum-create-field">
|
||||
<label>{isEnglish ? 'Select Category' : '选择分类'}</label>
|
||||
<label>{t('forum.selectCategory')}</label>
|
||||
<div className="forum-create-categories">
|
||||
{categories.map(cat => (
|
||||
<button
|
||||
@@ -256,13 +257,13 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
|
||||
{/* 标题 | Title */}
|
||||
<div className="forum-create-field">
|
||||
<label>{isEnglish ? 'Title' : '标题'}</label>
|
||||
<label>{t('forum.title')}</label>
|
||||
<div className="forum-create-title-input">
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder={isEnglish ? 'Enter a descriptive title...' : '输入一个描述性的标题...'}
|
||||
placeholder={t('forum.enterDescriptiveTitle')}
|
||||
maxLength={200}
|
||||
/>
|
||||
<span className="forum-create-count">{title.length}/200</span>
|
||||
@@ -279,7 +280,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
onClick={() => setActiveTab('write')}
|
||||
>
|
||||
<Edit3 size={14} />
|
||||
<span>{isEnglish ? 'Write' : '编辑'}</span>
|
||||
<span>{t('forum.write')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -287,7 +288,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
onClick={() => setActiveTab('preview')}
|
||||
>
|
||||
<Eye size={14} />
|
||||
<span>{isEnglish ? 'Preview' : '预览'}</span>
|
||||
<span>{t('forum.preview')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -309,7 +310,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="forum-editor-help"
|
||||
title={isEnglish ? 'Markdown Help' : 'Markdown 帮助'}
|
||||
title={t('forum.markdownHelp')}
|
||||
>
|
||||
<HelpCircle size={14} />
|
||||
</a>
|
||||
@@ -336,7 +337,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
{uploading && (
|
||||
<div className="forum-editor-upload-overlay">
|
||||
<Loader2 size={24} className="spin" />
|
||||
<span>{isEnglish ? 'Uploading...' : '上传中...'} {uploadProgress}%</span>
|
||||
<span>{t('forum.uploading')} {uploadProgress}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -344,7 +345,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
{isDragging && !uploading && (
|
||||
<div className="forum-editor-drag-overlay">
|
||||
<Upload size={32} />
|
||||
<span>{isEnglish ? 'Drop image here' : '拖放图片到这里'}</span>
|
||||
<span>{t('forum.dropImageHere')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -355,9 +356,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
onPaste={handlePaste}
|
||||
placeholder={isEnglish
|
||||
? 'Write your content here...\n\nYou can use Markdown:\n- **bold** and *italic*\n- `code` and ```code blocks```\n- [links](url) and \n- > quotes and - lists\n\nDrag & drop or paste images to upload'
|
||||
: '在这里写下你的内容...\n\n支持 Markdown 语法:\n- **粗体** 和 *斜体*\n- `代码` 和 ```代码块```\n- [链接](url) 和 \n- > 引用 和 - 列表\n\n拖拽或粘贴图片即可上传'}
|
||||
placeholder={t('forum.editorPlaceholder')}
|
||||
/>
|
||||
) : (
|
||||
<div className="forum-editor-preview">
|
||||
@@ -367,7 +366,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
<p className="forum-editor-preview-empty">
|
||||
{isEnglish ? 'Nothing to preview' : '暂无内容可预览'}
|
||||
{t('forum.nothingToPreview')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -391,7 +390,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
onClick={onBack}
|
||||
disabled={submitting}
|
||||
>
|
||||
{isEnglish ? 'Cancel' : '取消'}
|
||||
{t('forum.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -400,9 +399,7 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
>
|
||||
<Send size={16} />
|
||||
<span>
|
||||
{submitting
|
||||
? (isEnglish ? 'Creating...' : '创建中...')
|
||||
: (isEnglish ? 'Create Discussion' : '创建讨论')}
|
||||
{submitting ? t('forum.creating') : t('forum.createDiscussion')}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -412,18 +409,18 @@ export function ForumCreatePost({ categories, isEnglish, onBack, onCreated }: Fo
|
||||
{/* 右侧:提示 | Right: Tips */}
|
||||
<div className="forum-create-sidebar">
|
||||
<div className="forum-create-tips">
|
||||
<h3>{isEnglish ? 'Tips' : '小贴士'}</h3>
|
||||
<h3>{t('forum.tips')}</h3>
|
||||
<ul>
|
||||
<li>{isEnglish ? 'Use a clear, descriptive title' : '使用清晰、描述性的标题'}</li>
|
||||
<li>{isEnglish ? 'Select the right category for your topic' : '为你的话题选择合适的分类'}</li>
|
||||
<li>{isEnglish ? 'Provide enough context and details' : '提供足够的背景和细节'}</li>
|
||||
<li>{isEnglish ? 'Use code blocks for code snippets' : '使用代码块展示代码'}</li>
|
||||
<li>{isEnglish ? 'Be respectful and constructive' : '保持尊重和建设性'}</li>
|
||||
<li>{t('forum.tip1')}</li>
|
||||
<li>{t('forum.tip2')}</li>
|
||||
<li>{t('forum.tip3')}</li>
|
||||
<li>{t('forum.tip4')}</li>
|
||||
<li>{t('forum.tip5')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="forum-create-markdown-guide">
|
||||
<h3>{isEnglish ? 'Markdown Guide' : 'Markdown 指南'}</h3>
|
||||
<h3>{t('forum.markdownGuide')}</h3>
|
||||
<div className="forum-create-markdown-examples">
|
||||
<div className="markdown-example">
|
||||
<code>**bold**</code>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Forum panel main component - GitHub Discussions
|
||||
*/
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MessageSquare, RefreshCw } from 'lucide-react';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { useForumAuth, useCategories, usePosts } from '../../hooks/useForum';
|
||||
import { ForumAuth } from './ForumAuth';
|
||||
import { ForumPostList } from './ForumPostList';
|
||||
@@ -20,7 +20,8 @@ type ForumView = 'list' | 'detail' | 'create';
|
||||
* 认证后的论坛内容组件 | Authenticated forum content component
|
||||
* 只有在用户认证后才会渲染,确保 hooks 能正常工作
|
||||
*/
|
||||
function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean }) {
|
||||
function ForumContent({ user }: { user: ForumUser }) {
|
||||
const { t } = useLocale();
|
||||
const { categories, refetch: refetchCategories } = useCategories();
|
||||
const [view, setView] = useState<ForumView>('list');
|
||||
const [selectedPostNumber, setSelectedPostNumber] = useState<number | null>(null);
|
||||
@@ -80,14 +81,14 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
|
||||
<div className="forum-header-left">
|
||||
<MessageSquare size={18} />
|
||||
<span className="forum-title">
|
||||
{isEnglish ? 'Community' : '社区'}
|
||||
{t('forum.community')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="forum-header-right">
|
||||
<div
|
||||
className="forum-user"
|
||||
onClick={() => setShowProfile(!showProfile)}
|
||||
title={isEnglish ? 'Click to view profile' : '点击查看资料'}
|
||||
title={t('forum.clickToViewProfile')}
|
||||
>
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
@@ -118,7 +119,6 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
|
||||
totalCount={totalCount}
|
||||
hasNextPage={pageInfo.hasNextPage}
|
||||
params={listParams}
|
||||
isEnglish={isEnglish}
|
||||
onViewPost={handleViewPost}
|
||||
onCreatePost={handleCreatePost}
|
||||
onCategoryChange={handleCategoryChange}
|
||||
@@ -130,7 +130,6 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
|
||||
{view === 'detail' && selectedPostNumber && (
|
||||
<ForumPostDetail
|
||||
postNumber={selectedPostNumber}
|
||||
isEnglish={isEnglish}
|
||||
currentUserId={user.id}
|
||||
onBack={handleBack}
|
||||
/>
|
||||
@@ -138,7 +137,6 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
|
||||
{view === 'create' && (
|
||||
<ForumCreatePost
|
||||
categories={categories}
|
||||
isEnglish={isEnglish}
|
||||
onBack={handleBack}
|
||||
onCreated={handlePostCreated}
|
||||
/>
|
||||
@@ -149,18 +147,16 @@ function ForumContent({ user, isEnglish }: { user: ForumUser; isEnglish: boolean
|
||||
}
|
||||
|
||||
export function ForumPanel() {
|
||||
const { i18n } = useTranslation();
|
||||
const { t } = useLocale();
|
||||
const { authState } = useForumAuth();
|
||||
|
||||
const isEnglish = i18n.language === 'en';
|
||||
|
||||
// 加载状态 | Loading state
|
||||
if (authState.status === 'loading') {
|
||||
return (
|
||||
<div className="forum-panel">
|
||||
<div className="forum-loading">
|
||||
<RefreshCw className="spin" size={24} />
|
||||
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
|
||||
<span>{t('forum.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -178,7 +174,7 @@ export function ForumPanel() {
|
||||
// 已登录状态 - 渲染内容组件 | Authenticated state - render content component
|
||||
return (
|
||||
<div className="forum-panel">
|
||||
<ForumContent user={authState.user} isEnglish={isEnglish} />
|
||||
<ForumContent user={authState.user} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Send, RefreshCw, CornerDownRight, ExternalLink, CheckCircle
|
||||
} from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { usePost, useReplies } from '../../hooks/useForum';
|
||||
import { getForumService } from '../../services/forum';
|
||||
import type { Reply } from '../../services/forum';
|
||||
@@ -16,12 +17,12 @@ import './ForumPostDetail.css';
|
||||
|
||||
interface ForumPostDetailProps {
|
||||
postNumber: number;
|
||||
isEnglish: boolean;
|
||||
currentUserId: string;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }: ForumPostDetailProps) {
|
||||
export function ForumPostDetail({ postNumber, currentUserId, onBack }: ForumPostDetailProps) {
|
||||
const { t } = useLocale();
|
||||
const { post, loading: postLoading, toggleUpvote, refetch: refetchPost } = usePost(postNumber);
|
||||
const { replies, loading: repliesLoading, createReply, refetch: refetchReplies } = useReplies(postNumber);
|
||||
const [replyContent, setReplyContent] = useState('');
|
||||
@@ -71,7 +72,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
{reply.isAnswer && (
|
||||
<span className="forum-reply-answer-badge">
|
||||
<CheckCircle size={12} />
|
||||
{isEnglish ? 'Answer' : '已采纳'}
|
||||
{t('forum.answer')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -99,7 +100,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
onClick={() => setReplyingTo(replyingTo === reply.id ? null : reply.id)}
|
||||
>
|
||||
<CornerDownRight size={14} />
|
||||
<span>{isEnglish ? 'Reply' : '回复'}</span>
|
||||
<span>{t('forum.reply')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -108,9 +109,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
<textarea
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder={isEnglish
|
||||
? `Reply to @${reply.author.login}...`
|
||||
: `回复 @${reply.author.login}...`}
|
||||
placeholder={t('forum.replyTo', { login: reply.author.login })}
|
||||
rows={2}
|
||||
/>
|
||||
<div className="forum-reply-form-actions">
|
||||
@@ -119,7 +118,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
className="forum-btn"
|
||||
onClick={() => { setReplyingTo(null); setReplyContent(''); }}
|
||||
>
|
||||
{isEnglish ? 'Cancel' : '取消'}
|
||||
{t('forum.cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -127,7 +126,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
disabled={!replyContent.trim() || submitting}
|
||||
>
|
||||
<Send size={14} />
|
||||
<span>{isEnglish ? 'Reply' : '回复'}</span>
|
||||
<span>{t('forum.reply')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -144,7 +143,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
<div className="forum-post-detail">
|
||||
<div className="forum-detail-loading">
|
||||
<RefreshCw className="spin" size={24} />
|
||||
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
|
||||
<span>{t('forum.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -155,7 +154,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
{/* 返回按钮 | Back button */}
|
||||
<button className="forum-back-btn" onClick={onBack}>
|
||||
<ArrowLeft size={18} />
|
||||
<span>{isEnglish ? 'Back to list' : '返回列表'}</span>
|
||||
<span>{t('forum.backToList')}</span>
|
||||
</button>
|
||||
|
||||
{/* 帖子内容 | Post content */}
|
||||
@@ -168,13 +167,13 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
{post.answerChosenAt && (
|
||||
<span className="forum-detail-answered">
|
||||
<CheckCircle size={14} />
|
||||
{isEnglish ? 'Answered' : '已解决'}
|
||||
{t('forum.answered')}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
className="forum-detail-external"
|
||||
onClick={() => openInGitHub(post.url)}
|
||||
title={isEnglish ? 'Open in GitHub' : '在 GitHub 中打开'}
|
||||
title={t('forum.openInGitHub')}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
<span>GitHub</span>
|
||||
@@ -221,7 +220,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
<h2 className="forum-replies-title">
|
||||
<MessageCircle size={18} />
|
||||
<span>
|
||||
{isEnglish ? 'Comments' : '评论'}
|
||||
{t('forum.comments')}
|
||||
{post.comments.totalCount > 0 && ` (${post.comments.totalCount})`}
|
||||
</span>
|
||||
</h2>
|
||||
@@ -232,7 +231,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
<textarea
|
||||
value={replyContent}
|
||||
onChange={(e) => setReplyContent(e.target.value)}
|
||||
placeholder={isEnglish ? 'Write a comment... (Markdown supported)' : '写下你的评论...(支持 Markdown)'}
|
||||
placeholder={t('forum.writeComment')}
|
||||
rows={3}
|
||||
/>
|
||||
<div className="forum-reply-form-actions">
|
||||
@@ -242,9 +241,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
disabled={!replyContent.trim() || submitting}
|
||||
>
|
||||
<Send size={14} />
|
||||
<span>{submitting
|
||||
? (isEnglish ? 'Posting...' : '发送中...')
|
||||
: (isEnglish ? 'Post Comment' : '发表评论')}</span>
|
||||
<span>{submitting ? t('forum.posting') : t('forum.postComment')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -258,7 +255,7 @@ export function ForumPostDetail({ postNumber, isEnglish, currentUserId, onBack }
|
||||
</div>
|
||||
) : replies.length === 0 ? (
|
||||
<div className="forum-replies-empty">
|
||||
<p>{isEnglish ? 'No comments yet. Be the first to comment!' : '暂无评论,来发表第一条评论吧!'}</p>
|
||||
<p>{t('forum.noCommentsYet')}</p>
|
||||
</div>
|
||||
) : (
|
||||
replies.map(reply => renderReply(reply))
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Lightbulb, HelpCircle, Megaphone, BarChart3, Github
|
||||
} from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import type { Post, Category, PostListParams } from '../../services/forum';
|
||||
import { parseEmoji } from './utils';
|
||||
import './ForumPostList.css';
|
||||
@@ -20,7 +21,6 @@ interface ForumPostListProps {
|
||||
totalCount: number;
|
||||
hasNextPage: boolean;
|
||||
params: PostListParams;
|
||||
isEnglish: boolean;
|
||||
onViewPost: (postNumber: number) => void;
|
||||
onCreatePost: () => void;
|
||||
onCategoryChange: (categoryId: string | undefined) => void;
|
||||
@@ -48,7 +48,6 @@ export function ForumPostList({
|
||||
totalCount,
|
||||
hasNextPage,
|
||||
params,
|
||||
isEnglish,
|
||||
onViewPost,
|
||||
onCreatePost,
|
||||
onCategoryChange,
|
||||
@@ -56,6 +55,7 @@ export function ForumPostList({
|
||||
onRefresh,
|
||||
onLoadMore
|
||||
}: ForumPostListProps) {
|
||||
const { t } = useLocale();
|
||||
const [searchInput, setSearchInput] = useState(params.search || '');
|
||||
|
||||
const handleSearchSubmit = (e: React.FormEvent) => {
|
||||
@@ -73,13 +73,13 @@ export function ForumPostList({
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
if (hours === 0) {
|
||||
const mins = Math.floor(diff / (1000 * 60));
|
||||
if (mins < 1) return isEnglish ? 'Just now' : '刚刚';
|
||||
return isEnglish ? `${mins}m ago` : `${mins}分钟前`;
|
||||
if (mins < 1) return t('forum.justNow');
|
||||
return t('forum.minutesAgo', { count: mins });
|
||||
}
|
||||
return isEnglish ? `${hours}h ago` : `${hours}小时前`;
|
||||
return t('forum.hoursAgo', { count: hours });
|
||||
}
|
||||
if (days === 1) return isEnglish ? 'Yesterday' : '昨天';
|
||||
if (days < 7) return isEnglish ? `${days}d ago` : `${days}天前`;
|
||||
if (days === 1) return t('forum.yesterday');
|
||||
if (days < 7) return t('forum.daysAgo', { count: days });
|
||||
return date.toLocaleDateString();
|
||||
};
|
||||
|
||||
@@ -108,21 +108,17 @@ export function ForumPostList({
|
||||
<div className="forum-welcome-banner">
|
||||
<div className="forum-welcome-content">
|
||||
<div className="forum-welcome-text">
|
||||
<h2>{isEnglish ? 'ESEngine Community' : 'ESEngine 社区'}</h2>
|
||||
<p>
|
||||
{isEnglish
|
||||
? 'Ask questions, share ideas, and connect with other developers'
|
||||
: '提出问题、分享想法,与其他开发者交流'}
|
||||
</p>
|
||||
<h2>{t('forum.communityTitle')}</h2>
|
||||
<p>{t('forum.askQuestionsShareIdeas')}</p>
|
||||
</div>
|
||||
<div className="forum-welcome-actions">
|
||||
<button className="forum-btn forum-btn-primary" onClick={onCreatePost}>
|
||||
<Plus size={14} />
|
||||
<span>{isEnglish ? 'New Discussion' : '发起讨论'}</span>
|
||||
<span>{t('forum.newDiscussion')}</span>
|
||||
</button>
|
||||
<button className="forum-btn forum-btn-github" onClick={openGitHubDiscussions}>
|
||||
<Github size={14} />
|
||||
<span>{isEnglish ? 'View on GitHub' : '在 GitHub 查看'}</span>
|
||||
<span>{t('forum.viewOnGitHub')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +152,7 @@ export function ForumPostList({
|
||||
value={params.categoryId || ''}
|
||||
onChange={(e) => onCategoryChange(e.target.value || undefined)}
|
||||
>
|
||||
<option value="">{isEnglish ? 'All Categories' : '全部分类'}</option>
|
||||
<option value="">{t('forum.allCategories')}</option>
|
||||
{categories.map(cat => (
|
||||
<option key={cat.id} value={cat.id}>
|
||||
{parseEmoji(cat.emoji)} {cat.name}
|
||||
@@ -168,7 +164,7 @@ export function ForumPostList({
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={isEnglish ? 'Search discussions...' : '搜索讨论...'}
|
||||
placeholder={t('forum.searchDiscussions')}
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
/>
|
||||
@@ -180,7 +176,7 @@ export function ForumPostList({
|
||||
className="forum-btn"
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
title={isEnglish ? 'Refresh' : '刷新'}
|
||||
title={t('forum.refresh')}
|
||||
>
|
||||
<RefreshCw size={14} className={loading ? 'spin' : ''} />
|
||||
</button>
|
||||
@@ -189,7 +185,7 @@ export function ForumPostList({
|
||||
onClick={onCreatePost}
|
||||
>
|
||||
<Plus size={14} />
|
||||
<span>{isEnglish ? 'New' : '发帖'}</span>
|
||||
<span>{t('forum.new')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,14 +194,14 @@ export function ForumPostList({
|
||||
<div className="forum-stats">
|
||||
<div className="forum-stats-left">
|
||||
<TrendingUp size={14} />
|
||||
<span>{totalCount} {isEnglish ? 'discussions' : '条讨论'}</span>
|
||||
<span>{totalCount} {t('forum.discussions')}</span>
|
||||
</div>
|
||||
{params.categoryId && (
|
||||
<button
|
||||
className="forum-stats-clear"
|
||||
onClick={() => onCategoryChange(undefined)}
|
||||
>
|
||||
{isEnglish ? 'Clear filter' : '清除筛选'}
|
||||
{t('forum.clearFilter')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -222,15 +218,15 @@ export function ForumPostList({
|
||||
{loading && posts.length === 0 ? (
|
||||
<div className="forum-posts-loading">
|
||||
<RefreshCw size={16} className="spin" />
|
||||
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
|
||||
<span>{t('forum.loading')}</span>
|
||||
</div>
|
||||
) : posts.length === 0 ? (
|
||||
<div className="forum-posts-empty">
|
||||
<MessageCircle size={32} />
|
||||
<p>{isEnglish ? 'No discussions yet' : '暂无讨论'}</p>
|
||||
<p>{t('forum.noDiscussionsYet')}</p>
|
||||
<button className="forum-btn forum-btn-primary" onClick={onCreatePost}>
|
||||
<Plus size={14} />
|
||||
<span>{isEnglish ? 'Start a discussion' : '发起讨论'}</span>
|
||||
<span>{t('forum.startADiscussion')}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -258,13 +254,13 @@ export function ForumPostList({
|
||||
{isRecentPost(post) && (
|
||||
<span className="forum-post-badge new">
|
||||
<Clock size={10} />
|
||||
{isEnglish ? 'New' : '新'}
|
||||
{t('forum.newBadge')}
|
||||
</span>
|
||||
)}
|
||||
{isHotPost(post) && (
|
||||
<span className="forum-post-badge hot">
|
||||
<Flame size={10} />
|
||||
{isEnglish ? 'Hot' : '热门'}
|
||||
{t('forum.hotBadge')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -272,7 +268,7 @@ export function ForumPostList({
|
||||
<button
|
||||
className="forum-post-external"
|
||||
onClick={(e) => openInGitHub(post.url, e)}
|
||||
title={isEnglish ? 'Open in GitHub' : '在 GitHub 中打开'}
|
||||
title={t('forum.openInGitHub')}
|
||||
>
|
||||
<ExternalLink size={12} />
|
||||
</button>
|
||||
@@ -306,7 +302,7 @@ export function ForumPostList({
|
||||
{post.answerChosenAt && (
|
||||
<span className="forum-post-answered">
|
||||
<CheckCircle size={12} />
|
||||
{isEnglish ? 'Answered' : '已解决'}
|
||||
{t('forum.answered')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -325,10 +321,10 @@ export function ForumPostList({
|
||||
{loading ? (
|
||||
<>
|
||||
<RefreshCw size={14} className="spin" />
|
||||
<span>{isEnglish ? 'Loading...' : '加载中...'}</span>
|
||||
<span>{t('forum.loading')}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>{isEnglish ? 'Load More' : '加载更多'}</span>
|
||||
<span>{t('forum.loadMore')}</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* 用户资料组件 - GitHub
|
||||
* User profile component - GitHub
|
||||
*/
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Github, LogOut, ExternalLink } from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLocale } from '../../hooks/useLocale';
|
||||
import { useForumAuth } from '../../hooks/useForum';
|
||||
import './ForumProfile.css';
|
||||
|
||||
@@ -13,10 +13,9 @@ interface ForumProfileProps {
|
||||
}
|
||||
|
||||
export function ForumProfile({ onClose }: ForumProfileProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const { t } = useLocale();
|
||||
const { authState, signOut } = useForumAuth();
|
||||
|
||||
const isEnglish = i18n.language === 'en';
|
||||
const user = authState.status === 'authenticated' ? authState.user : null;
|
||||
|
||||
const handleSignOut = async () => {
|
||||
@@ -47,7 +46,7 @@ export function ForumProfile({ onClose }: ForumProfileProps) {
|
||||
onClick={openGitHubProfile}
|
||||
>
|
||||
<Github size={12} />
|
||||
<span>{isEnglish ? 'View GitHub Profile' : '查看 GitHub 主页'}</span>
|
||||
<span>{t('forum.viewGitHubProfile')}</span>
|
||||
<ExternalLink size={10} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -58,7 +57,7 @@ export function ForumProfile({ onClose }: ForumProfileProps) {
|
||||
<div className="forum-profile-actions">
|
||||
<button className="forum-profile-btn logout" onClick={handleSignOut}>
|
||||
<LogOut size={14} />
|
||||
<span>{isEnglish ? 'Sign Out' : '退出登录'}</span>
|
||||
<span>{t('forum.signOut')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { LocaleService, type Locale } from '@esengine/editor-core';
|
||||
import { LocaleService, type TranslationParams, type LocaleInfo, type Locale } from '@esengine/editor-core';
|
||||
|
||||
// Re-export Locale type for convenience | 重新导出 Locale 类型以便使用
|
||||
export type { Locale } from '@esengine/editor-core';
|
||||
|
||||
/**
|
||||
* React Hook for internationalization
|
||||
* React 国际化 Hook
|
||||
*
|
||||
* 提供翻译函数、语言切换和语言变化监听。
|
||||
* Provides translation function, locale switching and locale change listening.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { t, locale, changeLocale, supportedLocales } = useLocale();
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <h1>{t('app.title')}</h1>
|
||||
* <p>{t('scene.savedSuccess', { name: 'MyScene' })}</p>
|
||||
* <select value={locale} onChange={(e) => changeLocale(e.target.value as Locale)}>
|
||||
* {supportedLocales.map((loc) => (
|
||||
* <option key={loc.code} value={loc.code}>{loc.nativeName}</option>
|
||||
* ))}
|
||||
* </select>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useLocale() {
|
||||
const localeService = useMemo(() => Core.services.resolve(LocaleService), []);
|
||||
const [locale, setLocale] = useState<Locale>(() => localeService.getCurrentLocale());
|
||||
@@ -14,17 +43,44 @@ export function useLocale() {
|
||||
return unsubscribe;
|
||||
}, [localeService]);
|
||||
|
||||
const t = useCallback((key: string, fallback?: string) => {
|
||||
return localeService.t(key, fallback);
|
||||
}, [localeService]);
|
||||
/**
|
||||
* 翻译函数
|
||||
* Translation function
|
||||
*
|
||||
* @param key - 翻译键 | Translation key
|
||||
* @param params - 可选参数,用于替换 {{key}} | Optional params for {{key}} substitution
|
||||
* @param fallback - 回退文本 | Fallback text
|
||||
*/
|
||||
const t = useCallback(
|
||||
(key: string, params?: TranslationParams, fallback?: string) => {
|
||||
return localeService.t(key, params, fallback);
|
||||
},
|
||||
[localeService]
|
||||
);
|
||||
|
||||
const changeLocale = useCallback((newLocale: Locale) => {
|
||||
localeService.setLocale(newLocale);
|
||||
/**
|
||||
* 切换语言
|
||||
* Change locale
|
||||
*/
|
||||
const changeLocale = useCallback(
|
||||
(newLocale: Locale) => {
|
||||
localeService.setLocale(newLocale);
|
||||
},
|
||||
[localeService]
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* Get supported locales
|
||||
*/
|
||||
const supportedLocales: readonly LocaleInfo[] = useMemo(() => {
|
||||
return localeService.getSupportedLocales();
|
||||
}, [localeService]);
|
||||
|
||||
return {
|
||||
locale,
|
||||
t,
|
||||
changeLocale
|
||||
changeLocale,
|
||||
supportedLocales
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1140
packages/editor-app/src/locales/es.ts
Normal file
1140
packages/editor-app/src/locales/es.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,10 @@
|
||||
/**
|
||||
* Editor Core Translations
|
||||
* 编辑器核心翻译
|
||||
*
|
||||
* 插件翻译请使用 LocaleService.extendTranslations() 注册
|
||||
* Plugin translations should be registered via LocaleService.extendTranslations()
|
||||
*/
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,22 +18,24 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
const settingsRegistry = services.resolve(SettingsRegistry);
|
||||
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'appearance',
|
||||
title: '外观',
|
||||
description: '配置编辑器的外观设置',
|
||||
title: '$pluginSettings.appearance.title',
|
||||
description: '$pluginSettings.appearance.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'font',
|
||||
title: '字体设置',
|
||||
description: '配置编辑器字体样式',
|
||||
title: '$pluginSettings.appearance.font.title',
|
||||
description: '$pluginSettings.appearance.font.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'editor.fontSize',
|
||||
label: '字体大小 (px)',
|
||||
label: '$pluginSettings.appearance.font.fontSize.label',
|
||||
type: 'range',
|
||||
defaultValue: 13,
|
||||
description: '编辑器界面的字体大小',
|
||||
description: '$pluginSettings.appearance.font.fontSize.description',
|
||||
min: 11,
|
||||
max: 18,
|
||||
step: 1
|
||||
@@ -42,15 +44,15 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
id: 'inspector',
|
||||
title: '检视器设置',
|
||||
description: '配置属性检视器显示',
|
||||
title: '$pluginSettings.appearance.inspector.title',
|
||||
description: '$pluginSettings.appearance.inspector.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'inspector.decimalPlaces',
|
||||
label: '数字小数位数',
|
||||
label: '$pluginSettings.appearance.inspector.decimalPlaces.label',
|
||||
type: 'number',
|
||||
defaultValue: 4,
|
||||
description: '数字类型属性显示的小数位数,设置为 -1 表示不限制',
|
||||
description: '$pluginSettings.appearance.inspector.decimalPlaces.description',
|
||||
min: -1,
|
||||
max: 10,
|
||||
step: 1
|
||||
@@ -59,15 +61,15 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
id: 'scriptEditor',
|
||||
title: '脚本编辑器',
|
||||
description: '配置用于打开脚本文件的外部编辑器',
|
||||
title: '$pluginSettings.appearance.scriptEditor.title',
|
||||
description: '$pluginSettings.appearance.scriptEditor.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'editor.scriptEditor',
|
||||
label: '脚本编辑器',
|
||||
label: '$pluginSettings.appearance.scriptEditor.editor.label',
|
||||
type: 'select',
|
||||
defaultValue: 'system',
|
||||
description: '双击脚本文件时使用的编辑器',
|
||||
description: '$pluginSettings.appearance.scriptEditor.editor.description',
|
||||
options: SettingsService.SCRIPT_EDITORS.map(editor => ({
|
||||
value: editor.id,
|
||||
label: editor.name
|
||||
@@ -75,11 +77,11 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
key: 'editor.customScriptEditorCommand',
|
||||
label: '自定义编辑器命令',
|
||||
label: '$pluginSettings.appearance.scriptEditor.customCommand.label',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '当选择"自定义"时,填写编辑器的命令行命令(如 notepad++)',
|
||||
placeholder: '例如:notepad++'
|
||||
description: '$pluginSettings.appearance.scriptEditor.customCommand.description',
|
||||
placeholder: '$pluginSettings.appearance.scriptEditor.customCommand.placeholder'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,22 +17,24 @@ class PluginConfigEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
const settingsRegistry = services.resolve(SettingsRegistry);
|
||||
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'plugins',
|
||||
title: '插件',
|
||||
description: '管理项目使用的插件',
|
||||
title: '$pluginSettings.plugins.title',
|
||||
description: '$pluginSettings.plugins.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'engine-plugins',
|
||||
title: '插件管理',
|
||||
description: '启用或禁用项目需要的插件。禁用不需要的插件可以减少打包体积。',
|
||||
title: '$pluginSettings.plugins.management.title',
|
||||
description: '$pluginSettings.plugins.management.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'project.enabledPlugins',
|
||||
label: '',
|
||||
label: '$pluginSettings.plugins.management.list.label',
|
||||
type: 'pluginList',
|
||||
defaultValue: [],
|
||||
description: ''
|
||||
description: '$pluginSettings.plugins.management.list.description'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,60 +26,62 @@ class ProfilerEditorModule implements IEditorModuleLoader {
|
||||
this.messageHub = services.resolve(MessageHub);
|
||||
|
||||
const settingsRegistry = services.resolve(SettingsRegistry);
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'profiler',
|
||||
title: '性能分析器',
|
||||
description: '配置性能分析器的行为和显示选项',
|
||||
title: '$pluginSettings.profiler.title',
|
||||
description: '$pluginSettings.profiler.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'connection',
|
||||
title: '连接设置',
|
||||
description: '配置WebSocket服务器连接参数',
|
||||
title: '$pluginSettings.profiler.connection.title',
|
||||
description: '$pluginSettings.profiler.connection.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'profiler.port',
|
||||
label: '监听端口',
|
||||
label: '$pluginSettings.profiler.connection.port.label',
|
||||
type: 'number',
|
||||
defaultValue: 8080,
|
||||
description: '性能分析器WebSocket服务器监听的端口号',
|
||||
placeholder: '8080',
|
||||
description: '$pluginSettings.profiler.connection.port.description',
|
||||
placeholder: '$pluginSettings.profiler.connection.port.placeholder',
|
||||
min: 1024,
|
||||
max: 65535,
|
||||
validator: {
|
||||
validate: (value: number) => value >= 1024 && value <= 65535,
|
||||
errorMessage: '端口号必须在1024到65535之间'
|
||||
errorMessage: '$pluginSettings.profiler.connection.port.errorMessage'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'profiler.autoStart',
|
||||
label: '自动启动服务器',
|
||||
label: '$pluginSettings.profiler.connection.autoStart.label',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '编辑器启动时自动启动性能分析器服务器'
|
||||
description: '$pluginSettings.profiler.connection.autoStart.description'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
title: '显示设置',
|
||||
description: '配置性能数据的显示选项',
|
||||
title: '$pluginSettings.profiler.display.title',
|
||||
description: '$pluginSettings.profiler.display.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'profiler.refreshInterval',
|
||||
label: '刷新间隔 (毫秒)',
|
||||
label: '$pluginSettings.profiler.display.refreshInterval.label',
|
||||
type: 'range',
|
||||
defaultValue: 100,
|
||||
description: '性能数据刷新的时间间隔',
|
||||
description: '$pluginSettings.profiler.display.refreshInterval.description',
|
||||
min: 50,
|
||||
max: 1000,
|
||||
step: 50
|
||||
},
|
||||
{
|
||||
key: 'profiler.maxDataPoints',
|
||||
label: '最大数据点数',
|
||||
label: '$pluginSettings.profiler.display.maxDataPoints.label',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
description: '图表中保留的最大历史数据点数量',
|
||||
description: '$pluginSettings.profiler.display.maxDataPoints.description',
|
||||
min: 10,
|
||||
max: 500
|
||||
}
|
||||
|
||||
@@ -58,42 +58,46 @@ class ProjectSettingsEditorModule implements IEditorModuleLoader {
|
||||
// Setup listener for UI design resolution changes
|
||||
this.setupSettingsListener();
|
||||
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'project',
|
||||
title: '项目',
|
||||
description: '项目级别的配置',
|
||||
title: '$pluginSettings.project.title',
|
||||
description: '$pluginSettings.project.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'ui-settings',
|
||||
title: 'UI 设置',
|
||||
description: '配置 UI 系统的基础参数',
|
||||
title: '$pluginSettings.project.uiSettings.title',
|
||||
description: '$pluginSettings.project.uiSettings.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'project.uiDesignResolution.width',
|
||||
label: '设计宽度',
|
||||
label: '$pluginSettings.project.uiSettings.designWidth.label',
|
||||
type: 'number',
|
||||
defaultValue: 1920,
|
||||
description: 'UI 画布的设计宽度(像素)',
|
||||
description: '$pluginSettings.project.uiSettings.designWidth.description',
|
||||
min: 320,
|
||||
max: 7680,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
key: 'project.uiDesignResolution.height',
|
||||
label: '设计高度',
|
||||
label: '$pluginSettings.project.uiSettings.designHeight.label',
|
||||
type: 'number',
|
||||
defaultValue: 1080,
|
||||
description: 'UI 画布的设计高度(像素)',
|
||||
description: '$pluginSettings.project.uiSettings.designHeight.description',
|
||||
min: 240,
|
||||
max: 4320,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
key: 'project.uiDesignResolution.preset',
|
||||
label: '分辨率预设',
|
||||
label: '$pluginSettings.project.uiSettings.resolutionPreset.label',
|
||||
type: 'select',
|
||||
defaultValue: '1920x1080',
|
||||
description: '选择常见的分辨率预设',
|
||||
description: '$pluginSettings.project.uiSettings.resolutionPreset.description',
|
||||
// Resolution preset options use static labels (not localized)
|
||||
// 分辨率预设选项使用静态标签(不本地化)
|
||||
options: UI_RESOLUTION_PRESETS.map(p => ({
|
||||
label: p.label,
|
||||
value: `${p.value.width}x${p.value.height}`
|
||||
@@ -103,17 +107,17 @@ class ProjectSettingsEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
title: '引擎模块',
|
||||
description: '管理项目使用的引擎模块。每个模块包含运行时组件和编辑器工具。禁用不需要的模块可以减小构建体积。',
|
||||
title: '$pluginSettings.project.modules.title',
|
||||
description: '$pluginSettings.project.modules.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'project.disabledModules',
|
||||
label: '模块列表',
|
||||
label: '$pluginSettings.project.modules.list.label',
|
||||
type: 'moduleList',
|
||||
// Default: no modules disabled (all enabled)
|
||||
// 默认:没有禁用的模块(全部启用)
|
||||
defaultValue: [],
|
||||
description: '取消勾选不需要的模块。核心模块不能禁用。新增的模块会自动启用。',
|
||||
description: '$pluginSettings.project.modules.list.description',
|
||||
// Custom props for moduleList type
|
||||
// Modules are loaded dynamically from ModuleRegistry (sizes from module.json)
|
||||
// 模块从 ModuleRegistry 动态加载(大小来自 module.json)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { NodeTemplate, NodeTemplates } from '@esengine/behavior-tree';
|
||||
import { Search, X, LucideIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
|
||||
interface QuickCreateMenuProps {
|
||||
visible: boolean;
|
||||
@@ -32,6 +33,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
onNodeSelect,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const selectedNodeRef = useRef<HTMLDivElement>(null);
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(false);
|
||||
@@ -48,11 +50,12 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
})
|
||||
: allTemplates;
|
||||
|
||||
const uncategorizedLabel = t('quickCreateMenu.uncategorized');
|
||||
const categoryGroups: CategoryGroup[] = React.useMemo(() => {
|
||||
const groups = new Map<string, NodeTemplate[]>();
|
||||
|
||||
filteredTemplates.forEach((template: NodeTemplate) => {
|
||||
const category = template.category || '未分类';
|
||||
const category = template.category || uncategorizedLabel;
|
||||
if (!groups.has(category)) {
|
||||
groups.set(category, []);
|
||||
}
|
||||
@@ -64,7 +67,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
templates,
|
||||
isExpanded: searchTextLower ? true : expandedCategories.has(category)
|
||||
})).sort((a, b) => a.category.localeCompare(b.category));
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower]);
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower, uncategorizedLabel]);
|
||||
|
||||
const flattenedTemplates = React.useMemo(() => {
|
||||
return categoryGroups.flatMap((group) =>
|
||||
@@ -86,10 +89,10 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (allTemplates.length > 0 && expandedCategories.size === 0) {
|
||||
const categories = new Set(allTemplates.map((t) => t.category || '未分类'));
|
||||
const categories = new Set(allTemplates.map((template) => template.category || uncategorizedLabel));
|
||||
setExpandedCategories(categories);
|
||||
}
|
||||
}, [allTemplates, expandedCategories.size]);
|
||||
}, [allTemplates, expandedCategories.size, uncategorizedLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoScroll && selectedNodeRef.current) {
|
||||
@@ -157,7 +160,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
<Search size={16} style={{ color: '#999', flexShrink: 0 }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索节点..."
|
||||
placeholder={t('quickCreateMenu.searchPlaceholder')}
|
||||
autoFocus
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
@@ -225,7 +228,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
color: '#666',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
未找到匹配的节点
|
||||
{t('quickCreateMenu.noMatchingNodes')}
|
||||
</div>
|
||||
) : (
|
||||
categoryGroups.map((group) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Play, Pause, Square, SkipForward, RotateCcw, Trash2, Undo, Redo, Box } from 'lucide-react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
|
||||
type ExecutionMode = 'idle' | 'running' | 'paused' | 'step';
|
||||
|
||||
@@ -36,6 +37,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
onClearCanvas,
|
||||
onToggleGizmos
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -67,12 +69,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="运行 (Play)"
|
||||
title={t('editorToolbar.play')}
|
||||
>
|
||||
<Play size={16} />
|
||||
</button>
|
||||
|
||||
{/* 暂停按钮 */}
|
||||
{/* 暂停按钮 | Pause button */}
|
||||
<button
|
||||
onClick={onPause}
|
||||
disabled={executionMode === 'idle'}
|
||||
@@ -88,12 +90,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title={executionMode === 'paused' ? '继续' : '暂停'}
|
||||
title={executionMode === 'paused' ? t('editorToolbar.resume') : t('editorToolbar.pause')}
|
||||
>
|
||||
{executionMode === 'paused' ? <Play size={16} /> : <Pause size={16} />}
|
||||
</button>
|
||||
|
||||
{/* 停止按钮 */}
|
||||
{/* 停止按钮 | Stop button */}
|
||||
<button
|
||||
onClick={onStop}
|
||||
disabled={executionMode === 'idle'}
|
||||
@@ -109,12 +111,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="停止"
|
||||
title={t('editorToolbar.stop')}
|
||||
>
|
||||
<Square size={16} />
|
||||
</button>
|
||||
|
||||
{/* 单步执行按钮 */}
|
||||
{/* 单步执行按钮 | Step forward button */}
|
||||
<button
|
||||
onClick={onStep}
|
||||
disabled={executionMode !== 'idle' && executionMode !== 'paused'}
|
||||
@@ -130,12 +132,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="单步执行"
|
||||
title={t('editorToolbar.stepForward')}
|
||||
>
|
||||
<SkipForward size={16} />
|
||||
</button>
|
||||
|
||||
{/* 重置按钮 */}
|
||||
{/* 重置按钮 | Reset button */}
|
||||
<button
|
||||
onClick={onReset}
|
||||
style={{
|
||||
@@ -150,7 +152,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="重置"
|
||||
title={t('editorToolbar.reset')}
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
@@ -162,7 +164,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
margin: '4px 0'
|
||||
}} />
|
||||
|
||||
{/* 重置视图按钮 */}
|
||||
{/* 重置视图按钮 | Reset view button */}
|
||||
<button
|
||||
onClick={onResetView}
|
||||
style={{
|
||||
@@ -177,13 +179,13 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
title="重置视图 (滚轮缩放, Alt+拖动平移)"
|
||||
title={t('editorToolbar.resetView')}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
View
|
||||
</button>
|
||||
|
||||
{/* 清空画布按钮 */}
|
||||
{/* 清空画布按钮 | Clear canvas button */}
|
||||
<button
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
@@ -197,14 +199,14 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
title="清空画布"
|
||||
title={t('editorToolbar.clearCanvas')}
|
||||
onClick={onClearCanvas}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
清空
|
||||
{t('editorToolbar.clear')}
|
||||
</button>
|
||||
|
||||
{/* Gizmo 开关按钮 */}
|
||||
{/* Gizmo 开关按钮 | Gizmo toggle button */}
|
||||
<button
|
||||
onClick={onToggleGizmos}
|
||||
style={{
|
||||
@@ -219,7 +221,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
title="显示/隐藏选择边框 (Gizmos)"
|
||||
title={t('editorToolbar.toggleGizmos')}
|
||||
>
|
||||
<Box size={14} />
|
||||
Gizmos
|
||||
@@ -233,7 +235,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
margin: '0 4px'
|
||||
}} />
|
||||
|
||||
{/* 撤销按钮 */}
|
||||
{/* 撤销按钮 | Undo button */}
|
||||
<button
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
@@ -249,12 +251,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="撤销 (Ctrl+Z)"
|
||||
title={t('editorToolbar.undo')}
|
||||
>
|
||||
<Undo size={16} />
|
||||
</button>
|
||||
|
||||
{/* 重做按钮 */}
|
||||
{/* 重做按钮 | Redo button */}
|
||||
<button
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
@@ -270,12 +272,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
|
||||
title={t('editorToolbar.redo')}
|
||||
>
|
||||
<Redo size={16} />
|
||||
</button>
|
||||
|
||||
{/* 状态指示器 */}
|
||||
{/* 状态指示器 | Status indicator */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
@@ -294,9 +296,9 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
executionMode === 'running' ? '#4caf50' :
|
||||
executionMode === 'paused' ? '#ff9800' : '#666'
|
||||
}} />
|
||||
{executionMode === 'idle' ? 'Idle' :
|
||||
executionMode === 'running' ? 'Running' :
|
||||
executionMode === 'paused' ? 'Paused' : 'Step'}
|
||||
{executionMode === 'idle' ? t('editorToolbar.idle') :
|
||||
executionMode === 'running' ? t('editorToolbar.running') :
|
||||
executionMode === 'paused' ? t('editorToolbar.paused') : t('editorToolbar.step')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { ServiceToken } from '@esengine/engine-core';
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
@@ -71,8 +72,13 @@ export interface IPluginAPI {
|
||||
getEntityStore(): EntityStoreService;
|
||||
/** 获取 MessageHub | Get MessageHub */
|
||||
getMessageHub(): MessageHub;
|
||||
/** 解析服务 | Resolve service */
|
||||
resolveService<T>(serviceType: any): T;
|
||||
/**
|
||||
* 解析服务 | Resolve service
|
||||
*
|
||||
* 支持 ServiceToken<T>(推荐)或传统的 class/symbol。
|
||||
* Supports ServiceToken<T> (recommended) or legacy class/symbol.
|
||||
*/
|
||||
resolveService<T>(serviceType: ServiceToken<T> | symbol | (new (...args: any[]) => T)): T;
|
||||
/** 获取 Core 实例 | Get Core instance */
|
||||
getCore(): typeof Core;
|
||||
}
|
||||
@@ -185,7 +191,16 @@ export class PluginSDKRegistry {
|
||||
}
|
||||
return messageHubInstance;
|
||||
},
|
||||
resolveService: <T>(serviceType: any): T => Core.services.resolve(serviceType) as T,
|
||||
resolveService: <T>(serviceType: ServiceToken<T> | symbol | (new (...args: any[]) => T)): T => {
|
||||
// 检测是否是 ServiceToken(具有 id: symbol 属性)
|
||||
// Detect if this is a ServiceToken (has id: symbol property)
|
||||
if (serviceType && typeof serviceType === 'object' && 'id' in serviceType && typeof serviceType.id === 'symbol') {
|
||||
return Core.services.resolve(serviceType.id) as T;
|
||||
}
|
||||
// 传统方式:直接使用 class 或 symbol
|
||||
// Legacy: use class or symbol directly
|
||||
return Core.services.resolve(serviceType as symbol) as T;
|
||||
},
|
||||
getCore: () => Core,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,14 +93,17 @@ export class SettingsService {
|
||||
/**
|
||||
* 支持的脚本编辑器类型
|
||||
* Supported script editor types
|
||||
*
|
||||
* 使用 nameKey 作为翻译键,如果没有 nameKey 则使用 name 作为显示名称
|
||||
* Use nameKey as translation key, fallback to name if no nameKey
|
||||
*/
|
||||
public static readonly SCRIPT_EDITORS = [
|
||||
{ id: 'system', name: 'System Default', nameZh: '系统默认', command: '' },
|
||||
{ id: 'vscode', name: 'Visual Studio Code', nameZh: 'Visual Studio Code', command: 'code' },
|
||||
{ id: 'cursor', name: 'Cursor', nameZh: 'Cursor', command: 'cursor' },
|
||||
{ id: 'webstorm', name: 'WebStorm', nameZh: 'WebStorm', command: 'webstorm' },
|
||||
{ id: 'sublime', name: 'Sublime Text', nameZh: 'Sublime Text', command: 'subl' },
|
||||
{ id: 'custom', name: 'Custom', nameZh: '自定义', command: '' }
|
||||
{ id: 'system', name: 'System Default', nameKey: 'settings.scriptEditor.systemDefault', command: '' },
|
||||
{ id: 'vscode', name: 'Visual Studio Code', command: 'code' },
|
||||
{ id: 'cursor', name: 'Cursor', command: 'cursor' },
|
||||
{ id: 'webstorm', name: 'WebStorm', command: 'webstorm' },
|
||||
{ id: 'sublime', name: 'Sublime Text', command: 'subl' },
|
||||
{ id: 'custom', name: 'Custom', nameKey: 'settings.scriptEditor.custom', command: '' }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,14 @@ export interface IDialogExtended extends IDialog {
|
||||
setLocale(locale: string): void;
|
||||
}
|
||||
|
||||
const dialogTranslations = {
|
||||
en: { confirm: 'Confirm', cancel: 'Cancel' },
|
||||
zh: { confirm: '确定', cancel: '取消' },
|
||||
es: { confirm: 'Confirmar', cancel: 'Cancelar' }
|
||||
} as const;
|
||||
|
||||
type LocaleKey = keyof typeof dialogTranslations;
|
||||
|
||||
@singleton()
|
||||
export class TauriDialogService implements IDialogExtended {
|
||||
private showConfirmCallback?: (data: ConfirmDialogData) => void;
|
||||
@@ -70,8 +78,10 @@ export class TauriDialogService implements IDialogExtended {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmText = this.locale === 'zh' ? '确定' : 'Confirm';
|
||||
const cancelText = this.locale === 'zh' ? '取消' : 'Cancel';
|
||||
const localeKey = (this.locale in dialogTranslations ? this.locale : 'en') as LocaleKey;
|
||||
const texts = dialogTranslations[localeKey];
|
||||
const confirmText = texts.confirm;
|
||||
const cancelText = texts.cancel;
|
||||
|
||||
this.showConfirmCallback({
|
||||
title,
|
||||
|
||||
Reference in New Issue
Block a user