热更新配置

This commit is contained in:
YHH
2025-10-16 22:26:50 +08:00
parent 9f16debd75
commit 011e43811a
13 changed files with 1030 additions and 4 deletions

View File

@@ -12,11 +12,13 @@ import { PluginManagerWindow } from './components/PluginManagerWindow';
import { ProfilerWindow } from './components/ProfilerWindow';
import { PortManager } from './components/PortManager';
import { SettingsWindow } from './components/SettingsWindow';
import { AboutDialog } from './components/AboutDialog';
import { Viewport } from './components/Viewport';
import { MenuBar } from './components/MenuBar';
import { DockContainer, DockablePanel } from './components/DockContainer';
import { TauriAPI } from './api/tauri';
import { SettingsService } from './services/SettingsService';
import { checkForUpdatesOnStartup } from './utils/updater';
import { useLocale } from './hooks/useLocale';
import { en, zh } from './locales';
import { Loader2, Globe } from 'lucide-react';
@@ -49,6 +51,7 @@ function App() {
const [showProfiler, setShowProfiler] = useState(false);
const [showPortManager, setShowPortManager] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showAbout, setShowAbout] = useState(false);
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
const [isProfilerMode, setIsProfilerMode] = useState(false);
@@ -193,6 +196,9 @@ function App() {
setUiRegistry(uiRegistry);
setSettingsRegistry(settingsRegistry);
setStatus(t('header.status.ready'));
// Check for updates on startup (after 3 seconds)
checkForUpdatesOnStartup();
} catch (error) {
console.error('Failed to initialize editor:', error);
setStatus(t('header.status.failed'));
@@ -326,6 +332,10 @@ function App() {
}
};
const handleOpenAbout = () => {
setShowAbout(true);
};
useEffect(() => {
if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) {
let corePanels: DockablePanel[];
@@ -491,6 +501,7 @@ function App() {
onOpenPortManager={() => setShowPortManager(true)}
onOpenSettings={() => setShowSettings(true)}
onToggleDevtools={handleToggleDevtools}
onOpenAbout={handleOpenAbout}
/>
<div className="header-right">
<button onClick={handleLocaleChange} className="toolbar-btn locale-btn" title={locale === 'en' ? '切换到中文' : 'Switch to English'}>
@@ -528,6 +539,10 @@ function App() {
{showSettings && settingsRegistry && (
<SettingsWindow onClose={() => setShowSettings(false)} settingsRegistry={settingsRegistry} />
)}
{showAbout && (
<AboutDialog onClose={() => setShowAbout(false)} locale={locale} />
)}
</div>
);
}

View File

@@ -0,0 +1,213 @@
import { useState, useEffect } from 'react';
import { X, RefreshCw, Check, AlertCircle, Download } from 'lucide-react';
import { checkForUpdates } from '../utils/updater';
import { getVersion } from '@tauri-apps/api/app';
import { open } from '@tauri-apps/plugin-shell';
import '../styles/AboutDialog.css';
interface AboutDialogProps {
onClose: () => void;
locale?: string;
}
export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
const [checking, setChecking] = useState(false);
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'latest' | 'error'>('idle');
const [version, setVersion] = useState<string>('1.0.0');
const [newVersion, setNewVersion] = useState<string>('');
useEffect(() => {
// Fetch version on mount
const fetchVersion = async () => {
try {
const currentVersion = await getVersion();
setVersion(currentVersion);
} catch (error) {
console.error('Failed to get version:', error);
}
};
fetchVersion();
}, []);
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
en: {
title: 'About ECS Framework Editor',
version: 'Version',
description: 'High-performance ECS framework editor for 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 Update',
close: 'Close',
copyright: '© 2025 ESEngine. All rights reserved.',
website: 'Website',
github: 'GitHub'
},
zh: {
title: '关于 ECS Framework Editor',
version: '版本',
description: '高性能 ECS 框架编辑器,用于游戏开发',
checkUpdate: '检查更新',
checking: '检查中...',
updateAvailable: '发现新版本',
latest: '您正在使用最新版本',
error: '检查更新失败',
download: '下载更新',
close: '关闭',
copyright: '© 2025 ESEngine. 保留所有权利。',
website: '官网',
github: 'GitHub'
}
};
return translations[locale]?.[key] || key;
};
const handleCheckUpdate = async () => {
setChecking(true);
setUpdateStatus('checking');
try {
const currentVersion = await getVersion();
setVersion(currentVersion);
// 使用我们的 updater 工具检查更新
const result = await checkForUpdates(false);
if (result.error) {
setUpdateStatus('error');
} else if (result.available) {
setUpdateStatus('available');
if (result.version) {
setNewVersion(result.version);
}
} else {
setUpdateStatus('latest');
}
} catch (error: any) {
console.error('Check update failed:', error);
setUpdateStatus('error');
} finally {
setChecking(false);
}
};
const getStatusIcon = () => {
switch (updateStatus) {
case 'checking':
return <RefreshCw size={16} className="animate-spin" />;
case 'available':
return <Download size={16} className="status-available" />;
case 'latest':
return <Check size={16} className="status-latest" />;
case 'error':
return <AlertCircle size={16} className="status-error" />;
default:
return null;
}
};
const getStatusText = () => {
switch (updateStatus) {
case 'checking':
return t('checking');
case 'available':
return `${t('updateAvailable')} (v${newVersion})`;
case 'latest':
return t('latest');
case 'error':
return t('error');
default:
return '';
}
};
const handleOpenGithub = async () => {
try {
await open('https://github.com/esengine/ecs-framework');
} catch (error) {
console.error('Failed to open GitHub link:', error);
}
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="about-dialog" onClick={(e) => e.stopPropagation()}>
<div className="about-header">
<h2>{t('title')}</h2>
<button className="close-btn" onClick={onClose}>
<X size={20} />
</button>
</div>
<div className="about-content">
<div className="about-logo">
<div className="logo-placeholder">ECS</div>
</div>
<div className="about-info">
<h3>ECS Framework Editor</h3>
<p className="about-version">
{t('version')}: {version}
</p>
<p className="about-description">
{t('description')}
</p>
</div>
<div className="about-update">
<button
className="update-btn"
onClick={handleCheckUpdate}
disabled={checking}
>
{checking ? (
<>
<RefreshCw size={16} className="animate-spin" />
<span>{t('checking')}</span>
</>
) : (
<>
<RefreshCw size={16} />
<span>{t('checkUpdate')}</span>
</>
)}
</button>
{updateStatus !== 'idle' && (
<div className={`update-status status-${updateStatus}`}>
{getStatusIcon()}
<span>{getStatusText()}</span>
</div>
)}
</div>
<div className="about-links">
<a
href="#"
onClick={(e) => {
e.preventDefault();
handleOpenGithub();
}}
className="about-link"
>
{t('github')}
</a>
</div>
<div className="about-footer">
<p>{t('copyright')}</p>
</div>
</div>
<div className="about-actions">
<button className="btn-primary" onClick={onClose}>
{t('close')}
</button>
</div>
</div>
</div>
);
}

View File

@@ -29,6 +29,7 @@ interface MenuBarProps {
onOpenPortManager?: () => void;
onOpenSettings?: () => void;
onToggleDevtools?: () => void;
onOpenAbout?: () => void;
}
export function MenuBar({
@@ -47,7 +48,8 @@ export function MenuBar({
onOpenProfiler,
onOpenPortManager,
onOpenSettings,
onToggleDevtools
onToggleDevtools,
onOpenAbout
}: MenuBarProps) {
const [openMenu, setOpenMenu] = useState<string | null>(null);
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
@@ -144,6 +146,7 @@ export function MenuBar({
settings: 'Settings',
help: 'Help',
documentation: 'Documentation',
checkForUpdates: 'Check for Updates',
about: 'About',
devtools: 'Developer Tools'
},
@@ -176,6 +179,7 @@ export function MenuBar({
settings: '设置',
help: '帮助',
documentation: '文档',
checkForUpdates: '检查更新',
about: '关于',
devtools: '开发者工具'
}
@@ -233,7 +237,8 @@ export function MenuBar({
help: [
{ label: t('documentation'), disabled: true },
{ separator: true },
{ label: t('about'), disabled: true }
{ label: t('checkForUpdates'), onClick: onOpenAbout },
{ label: t('about'), onClick: onOpenAbout }
]
};

View File

@@ -0,0 +1,258 @@
.modal-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: 10000;
backdrop-filter: blur(4px);
}
.about-dialog {
background: var(--color-bg-elevated);
border-radius: 8px;
padding: 0;
width: 500px;
max-width: 90vw;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
animation: slideIn 0.2s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.about-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border-default);
}
.about-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
}
.about-header .close-btn {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.about-header .close-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.about-content {
padding: 32px 24px;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.about-logo {
width: 80px;
height: 80px;
}
.logo-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #569CD6, #4EC9B0);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: bold;
color: white;
box-shadow: 0 4px 12px rgba(86, 156, 214, 0.3);
}
.about-info {
text-align: center;
}
.about-info h3 {
margin: 0 0 8px 0;
font-size: 20px;
font-weight: 600;
color: var(--color-text-primary);
}
.about-version {
margin: 0 0 12px 0;
font-size: 14px;
color: var(--color-text-secondary);
}
.about-description {
margin: 0;
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.5;
max-width: 400px;
}
.about-update {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
}
.update-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 24px;
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 6px;
color: var(--color-text-primary);
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.update-btn:hover:not(:disabled) {
background: var(--color-bg-hover);
border-color: var(--color-primary);
}
.update-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.update-status {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
}
.update-status.status-checking {
background: rgba(86, 156, 214, 0.1);
color: #569CD6;
}
.update-status.status-available {
background: rgba(78, 201, 176, 0.1);
color: #4EC9B0;
}
.update-status.status-latest {
background: rgba(78, 201, 176, 0.1);
color: #4EC9B0;
}
.update-status.status-error {
background: rgba(206, 145, 120, 0.1);
color: #CE9178;
}
.update-status .status-available {
color: #4EC9B0;
}
.update-status .status-latest {
color: #4EC9B0;
}
.update-status .status-error {
color: #CE9178;
}
.about-links {
display: flex;
gap: 16px;
margin-top: 8px;
}
.about-link {
color: var(--color-primary);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.about-link:hover {
color: var(--color-primary-hover);
text-decoration: underline;
}
.about-footer {
margin-top: 8px;
padding-top: 16px;
border-top: 1px solid var(--color-border-default);
text-align: center;
}
.about-footer p {
margin: 0;
font-size: 12px;
color: var(--color-text-tertiary);
}
.about-actions {
padding: 16px 24px;
border-top: 1px solid var(--color-border-default);
display: flex;
justify-content: flex-end;
}
.btn-primary {
padding: 8px 24px;
background: var(--color-primary);
border: none;
border-radius: 6px;
color: white;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: var(--color-primary-hover);
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}

View File

@@ -0,0 +1,60 @@
import { check } from '@tauri-apps/plugin-updater';
export interface UpdateCheckResult {
available: boolean;
version?: string;
currentVersion?: string;
error?: string;
}
/**
* 检查应用更新
*
* 自动检查 GitHub Releases 是否有新版本
* 如果有更新,提示用户并可选择安装
*/
export async function checkForUpdates(silent: boolean = false): Promise<UpdateCheckResult> {
try {
const update = await check();
if (update?.available) {
console.log(`发现新版本: ${update.version}`);
console.log(`当前版本: ${update.currentVersion}`);
console.log(`更新日期: ${update.date}`);
console.log(`更新说明:\n${update.body}`);
if (!silent) {
// Tauri 会自动显示更新对话框(因为配置了 dialog: true
// 用户点击确认后会自动下载并安装,安装完成后会自动重启
await update.downloadAndInstall();
}
return {
available: true,
version: update.version,
currentVersion: update.currentVersion
};
} else {
if (!silent) {
console.log('当前已是最新版本');
}
return { available: false };
}
} catch (error) {
console.error('检查更新失败:', error);
return {
available: false,
error: error instanceof Error ? error.message : '检查更新失败'
};
}
}
/**
* 应用启动时静默检查更新
*/
export async function checkForUpdatesOnStartup(): Promise<void> {
// 延迟 3 秒后检查,避免影响启动速度
setTimeout(() => {
checkForUpdates(true);
}, 3000);
}