fix: 修复多个编辑器问题
This commit is contained in:
@@ -75,10 +75,35 @@ pub fn open_file_with_default_app(file_path: String) -> Result<(), String> {
|
||||
/// Show file in system file explorer
|
||||
#[tauri::command]
|
||||
pub fn show_in_folder(file_path: String) -> Result<(), String> {
|
||||
println!("[show_in_folder] Received path: {}", file_path);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::path::Path;
|
||||
|
||||
// Normalize path separators for Windows
|
||||
// 规范化路径分隔符
|
||||
let normalized_path = file_path.replace('/', "\\");
|
||||
println!("[show_in_folder] Normalized path: {}", normalized_path);
|
||||
|
||||
// Verify the path exists before trying to show it
|
||||
// 验证路径存在
|
||||
let path = Path::new(&normalized_path);
|
||||
let exists = path.exists();
|
||||
println!("[show_in_folder] Path exists: {}", exists);
|
||||
|
||||
if !exists {
|
||||
return Err(format!("Path does not exist: {}", normalized_path));
|
||||
}
|
||||
|
||||
// Windows explorer requires /select, to be concatenated with the path
|
||||
// without spaces. Use a single argument to avoid shell parsing issues.
|
||||
// Windows 资源管理器要求 /select, 与路径连接在一起,中间没有空格
|
||||
let select_arg = format!("/select,{}", normalized_path);
|
||||
println!("[show_in_folder] Explorer arg: {}", select_arg);
|
||||
|
||||
Command::new("explorer")
|
||||
.args(["/select,", &file_path])
|
||||
.arg(&select_arg)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||||
}
|
||||
|
||||
@@ -465,7 +465,9 @@ function App() {
|
||||
};
|
||||
|
||||
const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => {
|
||||
const fullProjectPath = `${projectPath}\\${projectName}`;
|
||||
// 使用与 projectPath 相同的路径分隔符 | Use same separator as projectPath
|
||||
const sep = projectPath.includes('/') ? '/' : '\\';
|
||||
const fullProjectPath = `${projectPath}${sep}${projectName}`;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -824,6 +826,28 @@ function App() {
|
||||
onOpenProject={handleOpenProject}
|
||||
onCreateProject={handleCreateProject}
|
||||
onOpenRecentProject={handleOpenRecentProject}
|
||||
onRemoveRecentProject={(projectPath) => {
|
||||
settings.removeRecentProject(projectPath);
|
||||
// 强制重新渲染 | Force re-render
|
||||
setStatus(t('header.status.ready'));
|
||||
}}
|
||||
onDeleteProject={async (projectPath) => {
|
||||
try {
|
||||
await TauriAPI.deleteFolder(projectPath);
|
||||
// 删除成功后从列表中移除并触发重新渲染
|
||||
// Remove from list and trigger re-render after successful deletion
|
||||
settings.removeRecentProject(projectPath);
|
||||
setStatus(t('header.status.ready'));
|
||||
} catch (error) {
|
||||
console.error('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)}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
recentProjects={recentProjects}
|
||||
locale={locale}
|
||||
|
||||
@@ -799,9 +799,10 @@ export function ContentBrowser({
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
console.log('[ContentBrowser] showInFolder path:', asset.path);
|
||||
await TauriAPI.showInFolder(asset.path);
|
||||
} catch (error) {
|
||||
console.error('Failed to show in folder:', error);
|
||||
console.error('Failed to show in folder:', error, 'Path:', asset.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -833,9 +833,10 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
icon: <FolderOpen size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
console.log('[FileTree] showInFolder path:', node.path);
|
||||
await TauriAPI.showInFolder(node.path);
|
||||
} catch (error) {
|
||||
console.error('Failed to show in folder:', error);
|
||||
console.error('Failed to show in folder:', error, 'Path:', node.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { Globe, ChevronDown, Download, X, Loader2 } from 'lucide-react';
|
||||
import { Globe, ChevronDown, Download, X, Loader2, Trash2 } from 'lucide-react';
|
||||
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
||||
import { StartupLogo } from './StartupLogo';
|
||||
import '../styles/StartupPage.css';
|
||||
@@ -11,6 +11,8 @@ interface StartupPageProps {
|
||||
onOpenProject: () => void;
|
||||
onCreateProject: () => void;
|
||||
onOpenRecentProject?: (projectPath: string) => void;
|
||||
onRemoveRecentProject?: (projectPath: string) => void;
|
||||
onDeleteProject?: (projectPath: string) => Promise<void>;
|
||||
onLocaleChange?: (locale: Locale) => void;
|
||||
recentProjects?: string[];
|
||||
locale: string;
|
||||
@@ -21,11 +23,13 @@ const LANGUAGES = [
|
||||
{ code: 'zh', name: '中文' }
|
||||
];
|
||||
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
const [showLogo, setShowLogo] = useState(true);
|
||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
const [showLangMenu, setShowLangMenu] = useState(false);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; project: string } | null>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null);
|
||||
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
@@ -66,7 +70,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
updateAvailable: 'New version available',
|
||||
updateNow: 'Update Now',
|
||||
installing: 'Installing...',
|
||||
later: 'Later'
|
||||
later: 'Later',
|
||||
removeFromList: 'Remove from List',
|
||||
deleteProject: 'Delete Project',
|
||||
deleteConfirmTitle: 'Delete Project',
|
||||
deleteConfirmMessage: 'Are you sure you want to permanently delete this project? This action cannot be undone.',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete'
|
||||
},
|
||||
zh: {
|
||||
title: 'ESEngine 编辑器',
|
||||
@@ -78,7 +88,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
updateAvailable: '发现新版本',
|
||||
updateNow: '立即更新',
|
||||
installing: '正在安装...',
|
||||
later: '稍后'
|
||||
later: '稍后',
|
||||
removeFromList: '从列表中移除',
|
||||
deleteProject: '删除项目',
|
||||
deleteConfirmTitle: '删除项目',
|
||||
deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
delete: '删除'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -136,6 +152,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
onMouseEnter={() => setHoveredProject(project)}
|
||||
onMouseLeave={() => setHoveredProject(null)}
|
||||
onClick={() => onOpenRecentProject?.(project)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, project });
|
||||
}}
|
||||
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
|
||||
>
|
||||
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
@@ -145,6 +165,18 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
|
||||
<div className="recent-path">{project}</div>
|
||||
</div>
|
||||
{onRemoveRecentProject && (
|
||||
<button
|
||||
className="recent-remove-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveRecentProject(project);
|
||||
}}
|
||||
title={t.removeFromList}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -217,6 +249,78 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右键菜单 | Context Menu */}
|
||||
{contextMenu && (
|
||||
<div
|
||||
className="startup-context-menu-overlay"
|
||||
onClick={() => setContextMenu(null)}
|
||||
>
|
||||
<div
|
||||
className="startup-context-menu"
|
||||
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="startup-context-menu-item"
|
||||
onClick={() => {
|
||||
onRemoveRecentProject?.(contextMenu.project);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
<span>{t.removeFromList}</span>
|
||||
</button>
|
||||
{onDeleteProject && (
|
||||
<button
|
||||
className="startup-context-menu-item danger"
|
||||
onClick={() => {
|
||||
setDeleteConfirm(contextMenu.project);
|
||||
setContextMenu(null);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>{t.deleteProject}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 删除确认对话框 | Delete Confirmation Dialog */}
|
||||
{deleteConfirm && (
|
||||
<div className="startup-dialog-overlay">
|
||||
<div className="startup-dialog">
|
||||
<div className="startup-dialog-header">
|
||||
<Trash2 size={20} className="dialog-icon-danger" />
|
||||
<h3>{t.deleteConfirmTitle}</h3>
|
||||
</div>
|
||||
<div className="startup-dialog-body">
|
||||
<p>{t.deleteConfirmMessage}</p>
|
||||
<p className="startup-dialog-path">{deleteConfirm}</p>
|
||||
</div>
|
||||
<div className="startup-dialog-footer">
|
||||
<button
|
||||
className="startup-dialog-btn"
|
||||
onClick={() => setDeleteConfirm(null)}
|
||||
>
|
||||
{t.cancel}
|
||||
</button>
|
||||
<button
|
||||
className="startup-dialog-btn danger"
|
||||
onClick={async () => {
|
||||
if (deleteConfirm && onDeleteProject) {
|
||||
await onDeleteProject(deleteConfirm);
|
||||
}
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
>
|
||||
{t.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,9 +70,11 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
public addRecentProject(projectPath: string): void {
|
||||
// 规范化路径,防止双重转义 | Normalize path to prevent double escaping
|
||||
const normalizedPath = projectPath.replace(/\\\\/g, '\\');
|
||||
const recentProjects = this.getRecentProjects();
|
||||
const filtered = recentProjects.filter((p) => p !== projectPath);
|
||||
const updated = [projectPath, ...filtered].slice(0, 10);
|
||||
const filtered = recentProjects.filter((p) => p !== normalizedPath);
|
||||
const updated = [normalizedPath, ...filtered].slice(0, 10);
|
||||
this.set('recentProjects', updated);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,6 +174,32 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recent-remove-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: #6e6e6e;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recent-item:hover .recent-remove-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.recent-remove-btn:hover {
|
||||
background: rgba(255, 80, 80, 0.15);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.startup-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -346,3 +372,157 @@
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 右键菜单样式 | Context Menu Styles */
|
||||
.startup-context-menu-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.startup-context-menu {
|
||||
position: fixed;
|
||||
min-width: 180px;
|
||||
background: #252529;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
padding: 4px 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.startup-context-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 14px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
.startup-context-menu-item:hover {
|
||||
background: #3b82f6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.startup-context-menu-item.danger {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.startup-context-menu-item.danger:hover {
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 对话框样式 | Dialog Styles */
|
||||
.startup-dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
.startup-dialog {
|
||||
width: 400px;
|
||||
background: #2d2d30;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.startup-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: #252526;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.startup-dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dialog-icon-danger {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.startup-dialog-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.startup-dialog-body p {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 13px;
|
||||
color: #cccccc;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.startup-dialog-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.startup-dialog-path {
|
||||
padding: 10px 12px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
color: #858585;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.startup-dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 16px 20px;
|
||||
background: #252526;
|
||||
border-top: 1px solid #3e3e42;
|
||||
}
|
||||
|
||||
.startup-dialog-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 4px;
|
||||
background: #2d2d30;
|
||||
color: #cccccc;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.startup-dialog-btn:hover {
|
||||
background: #37373d;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.startup-dialog-btn.danger {
|
||||
background: #dc2626;
|
||||
border-color: #dc2626;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.startup-dialog-btn.danger:hover {
|
||||
background: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
|
||||
@@ -94,11 +94,15 @@ export class ProjectService implements IService {
|
||||
scriptsPath: 'scripts',
|
||||
buildOutput: '.esengine/compiled',
|
||||
scenesPath: 'scenes',
|
||||
defaultScene: 'main.ecs'
|
||||
defaultScene: 'main.ecs',
|
||||
plugins: { enabledPlugins: [] },
|
||||
modules: { disabledModules: [] }
|
||||
};
|
||||
|
||||
await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
// Create scenes folder and default scene
|
||||
// 创建场景文件夹和默认场景
|
||||
const scenesPath = `${projectPath}${sep}${config.scenesPath}`;
|
||||
await this.fileAPI.createDirectory(scenesPath);
|
||||
|
||||
@@ -111,6 +115,21 @@ export class ProjectService implements IService {
|
||||
}) as string;
|
||||
await this.fileAPI.writeFileContent(defaultScenePath, sceneData);
|
||||
|
||||
// Create scripts folder for user scripts
|
||||
// 创建用户脚本文件夹
|
||||
const scriptsPath = `${projectPath}${sep}${config.scriptsPath}`;
|
||||
await this.fileAPI.createDirectory(scriptsPath);
|
||||
|
||||
// Create scripts/editor folder for editor extension scripts
|
||||
// 创建编辑器扩展脚本文件夹
|
||||
const editorScriptsPath = `${scriptsPath}${sep}editor`;
|
||||
await this.fileAPI.createDirectory(editorScriptsPath);
|
||||
|
||||
// Create assets folder for project assets (textures, audio, etc.)
|
||||
// 创建资源文件夹(纹理、音频等)
|
||||
const assetsPath = `${projectPath}${sep}assets`;
|
||||
await this.fileAPI.createDirectory(assetsPath);
|
||||
|
||||
await this.messageHub.publish('project:created', {
|
||||
path: projectPath
|
||||
});
|
||||
@@ -258,8 +277,10 @@ export class ProjectService implements IService {
|
||||
scenesPath: config.scenesPath || 'scenes',
|
||||
defaultScene: config.defaultScene || 'main.ecs',
|
||||
uiDesignResolution: config.uiDesignResolution,
|
||||
plugins: config.plugins,
|
||||
modules: config.modules
|
||||
// Provide default empty plugins config for legacy projects
|
||||
// 为旧项目提供默认的空插件配置
|
||||
plugins: config.plugins || { enabledPlugins: [] },
|
||||
modules: config.modules || { disabledModules: [] }
|
||||
};
|
||||
logger.debug('Loaded config result:', result);
|
||||
return result;
|
||||
|
||||
Reference in New Issue
Block a user