设置界面

This commit is contained in:
YHH
2025-10-16 13:07:19 +08:00
parent 6bcfd48a2f
commit 1ec7892338
11 changed files with 1141 additions and 6 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from 'react';
import { Core, Scene } from '@esengine/ecs-framework'; import { Core, Scene } from '@esengine/ecs-framework';
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService, LogService } from '@esengine/editor-core'; import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, ComponentLoaderService, PropertyMetadataService, LogService, SettingsRegistry } from '@esengine/editor-core';
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin'; import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
import { ProfilerPlugin } from './plugins/ProfilerPlugin'; import { ProfilerPlugin } from './plugins/ProfilerPlugin';
import { StartupPage } from './components/StartupPage'; import { StartupPage } from './components/StartupPage';
@@ -12,6 +12,7 @@ import { ProfilerPanel } from './components/ProfilerPanel';
import { PluginManagerWindow } from './components/PluginManagerWindow'; import { PluginManagerWindow } from './components/PluginManagerWindow';
import { ProfilerWindow } from './components/ProfilerWindow'; import { ProfilerWindow } from './components/ProfilerWindow';
import { PortManager } from './components/PortManager'; import { PortManager } from './components/PortManager';
import { SettingsWindow } from './components/SettingsWindow';
import { Viewport } from './components/Viewport'; import { Viewport } from './components/Viewport';
import { MenuBar } from './components/MenuBar'; import { MenuBar } from './components/MenuBar';
import { DockContainer, DockablePanel } from './components/DockContainer'; import { DockContainer, DockablePanel } from './components/DockContainer';
@@ -40,12 +41,14 @@ function App() {
const [messageHub, setMessageHub] = useState<MessageHub | null>(null); const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
const [logService, setLogService] = useState<LogService | null>(null); const [logService, setLogService] = useState<LogService | null>(null);
const [uiRegistry, setUiRegistry] = useState<UIRegistry | null>(null); const [uiRegistry, setUiRegistry] = useState<UIRegistry | null>(null);
const [settingsRegistry, setSettingsRegistry] = useState<SettingsRegistry | null>(null);
const { t, locale, changeLocale } = useLocale(); const { t, locale, changeLocale } = useLocale();
const [status, setStatus] = useState(t('header.status.initializing')); const [status, setStatus] = useState(t('header.status.initializing'));
const [panels, setPanels] = useState<DockablePanel[]>([]); const [panels, setPanels] = useState<DockablePanel[]>([]);
const [showPluginManager, setShowPluginManager] = useState(false); const [showPluginManager, setShowPluginManager] = useState(false);
const [showProfiler, setShowProfiler] = useState(false); const [showProfiler, setShowProfiler] = useState(false);
const [showPortManager, setShowPortManager] = useState(false); const [showPortManager, setShowPortManager] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0); const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
const [isRemoteConnected, setIsRemoteConnected] = useState(false); const [isRemoteConnected, setIsRemoteConnected] = useState(false);
@@ -139,6 +142,7 @@ function App() {
const componentLoader = new ComponentLoaderService(messageHub, componentRegistry); const componentLoader = new ComponentLoaderService(messageHub, componentRegistry);
const propertyMetadata = new PropertyMetadataService(); const propertyMetadata = new PropertyMetadataService();
const logService = new LogService(); const logService = new LogService();
const settingsRegistry = new SettingsRegistry();
Core.services.registerInstance(UIRegistry, uiRegistry); Core.services.registerInstance(UIRegistry, uiRegistry);
Core.services.registerInstance(MessageHub, messageHub); Core.services.registerInstance(MessageHub, messageHub);
@@ -150,6 +154,7 @@ function App() {
Core.services.registerInstance(ComponentLoaderService, componentLoader); Core.services.registerInstance(ComponentLoaderService, componentLoader);
Core.services.registerInstance(PropertyMetadataService, propertyMetadata); Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
Core.services.registerInstance(LogService, logService); Core.services.registerInstance(LogService, logService);
Core.services.registerInstance(SettingsRegistry, settingsRegistry);
const pluginMgr = new EditorPluginManager(); const pluginMgr = new EditorPluginManager();
pluginMgr.initialize(coreInstance, Core.services); pluginMgr.initialize(coreInstance, Core.services);
@@ -180,6 +185,7 @@ function App() {
setMessageHub(messageHub); setMessageHub(messageHub);
setLogService(logService); setLogService(logService);
setUiRegistry(uiRegistry); setUiRegistry(uiRegistry);
setSettingsRegistry(settingsRegistry);
setStatus(t('header.status.ready')); setStatus(t('header.status.ready'));
} catch (error) { } catch (error) {
console.error('Failed to initialize editor:', error); console.error('Failed to initialize editor:', error);
@@ -426,6 +432,7 @@ function App() {
onOpenPluginManager={() => setShowPluginManager(true)} onOpenPluginManager={() => setShowPluginManager(true)}
onOpenProfiler={() => setShowProfiler(true)} onOpenProfiler={() => setShowProfiler(true)}
onOpenPortManager={() => setShowPortManager(true)} onOpenPortManager={() => setShowPortManager(true)}
onOpenSettings={() => setShowSettings(true)}
onToggleDevtools={handleToggleDevtools} onToggleDevtools={handleToggleDevtools}
/> />
<div className="header-right"> <div className="header-right">
@@ -460,6 +467,10 @@ function App() {
{showPortManager && ( {showPortManager && (
<PortManager onClose={() => setShowPortManager(false)} /> <PortManager onClose={() => setShowPortManager(false)} />
)} )}
{showSettings && settingsRegistry && (
<SettingsWindow onClose={() => setShowSettings(false)} settingsRegistry={settingsRegistry} />
)}
</div> </div>
); );
} }

View File

@@ -27,6 +27,7 @@ interface MenuBarProps {
onOpenPluginManager?: () => void; onOpenPluginManager?: () => void;
onOpenProfiler?: () => void; onOpenProfiler?: () => void;
onOpenPortManager?: () => void; onOpenPortManager?: () => void;
onOpenSettings?: () => void;
onToggleDevtools?: () => void; onToggleDevtools?: () => void;
} }
@@ -45,6 +46,7 @@ export function MenuBar({
onOpenPluginManager, onOpenPluginManager,
onOpenProfiler, onOpenProfiler,
onOpenPortManager, onOpenPortManager,
onOpenSettings,
onToggleDevtools onToggleDevtools
}: MenuBarProps) { }: MenuBarProps) {
const [openMenu, setOpenMenu] = useState<string | null>(null); const [openMenu, setOpenMenu] = useState<string | null>(null);
@@ -139,6 +141,7 @@ export function MenuBar({
pluginManager: 'Plugin Manager', pluginManager: 'Plugin Manager',
tools: 'Tools', tools: 'Tools',
portManager: 'Port Manager', portManager: 'Port Manager',
settings: 'Settings',
help: 'Help', help: 'Help',
documentation: 'Documentation', documentation: 'Documentation',
about: 'About', about: 'About',
@@ -170,6 +173,7 @@ export function MenuBar({
pluginManager: '插件管理器', pluginManager: '插件管理器',
tools: '工具', tools: '工具',
portManager: '端口管理器', portManager: '端口管理器',
settings: '设置',
help: '帮助', help: '帮助',
documentation: '文档', documentation: '文档',
about: '关于', about: '关于',
@@ -222,7 +226,9 @@ export function MenuBar({
{ label: t('devtools'), onClick: onToggleDevtools } { label: t('devtools'), onClick: onToggleDevtools }
], ],
tools: [ tools: [
{ label: t('portManager'), onClick: onOpenPortManager } { label: t('portManager'), onClick: onOpenPortManager },
{ separator: true },
{ label: t('settings'), onClick: onOpenSettings }
], ],
help: [ help: [
{ label: t('documentation'), disabled: true }, { label: t('documentation'), disabled: true },

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2 } from 'lucide-react'; import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2 } from 'lucide-react';
import { ProfilerService, ProfilerData } from '../services/ProfilerService'; import { ProfilerService, ProfilerData } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core'; import { MessageHub } from '@esengine/editor-core';
import '../styles/ProfilerDockPanel.css'; import '../styles/ProfilerDockPanel.css';
@@ -9,6 +10,25 @@ export function ProfilerDockPanel() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null); const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false); const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
useEffect(() => { useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
@@ -99,7 +119,7 @@ export function ProfilerDockPanel() {
<div className="profiler-dock-empty"> <div className="profiler-dock-empty">
<Activity size={32} /> <Activity size={32} />
<p>Waiting for game connection...</p> <p>Waiting for game connection...</p>
<p className="hint">Connect your game to port 8080</p> <p className="hint">Connect to: <code>ws://localhost:{port}</code></p>
</div> </div>
) : ( ) : (
<div className="profiler-dock-content"> <div className="profiler-dock-content">

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react'; import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService'; import { ProfilerService } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import '../styles/ProfilerWindow.css'; import '../styles/ProfilerWindow.css';
interface SystemPerformanceData { interface SystemPerformanceData {
@@ -33,8 +34,27 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false); const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const animationRef = useRef<number>(); const animationRef = useRef<number>();
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
// Check ProfilerService connection status // Check ProfilerService connection status
useEffect(() => { useEffect(() => {
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined; const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
@@ -361,7 +381,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
<div className="profiler-connection"> <div className="profiler-connection">
<div className="connection-port-display"> <div className="connection-port-display">
<Server size={14} /> <Server size={14} />
<span>Port: 8080</span> <span>ws://localhost:{port}</span>
</div> </div>
{isConnected ? ( {isConnected ? (
<div className="connection-status-indicator connected"> <div className="connection-status-indicator connected">

View File

@@ -0,0 +1,295 @@
import { useState, useEffect } from 'react';
import { X, Settings as SettingsIcon, ChevronRight } from 'lucide-react';
import { SettingsService } from '../services/SettingsService';
import { SettingsRegistry, SettingCategory, SettingDescriptor } from '@esengine/editor-core';
import '../styles/SettingsWindow.css';
interface SettingsWindowProps {
onClose: () => void;
settingsRegistry: SettingsRegistry;
}
export function SettingsWindow({ onClose, settingsRegistry }: SettingsWindowProps) {
const [categories, setCategories] = useState<SettingCategory[]>([]);
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null);
const [values, setValues] = useState<Map<string, any>>(new Map());
const [errors, setErrors] = useState<Map<string, string>>(new Map());
useEffect(() => {
const allCategories = settingsRegistry.getAllCategories();
setCategories(allCategories);
if (allCategories.length > 0 && !selectedCategoryId) {
const firstCategory = allCategories[0];
if (firstCategory) {
setSelectedCategoryId(firstCategory.id);
}
}
const settings = SettingsService.getInstance();
const allSettings = settingsRegistry.getAllSettings();
const initialValues = new Map<string, any>();
for (const [key, descriptor] of allSettings.entries()) {
const value = settings.get(key, descriptor.defaultValue);
initialValues.set(key, value);
}
setValues(initialValues);
}, [settingsRegistry, selectedCategoryId]);
const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => {
const newValues = new Map(values);
newValues.set(key, value);
setValues(newValues);
const newErrors = new Map(errors);
if (!settingsRegistry.validateSetting(descriptor, value)) {
newErrors.set(key, descriptor.validator?.errorMessage || 'Invalid value');
} else {
newErrors.delete(key);
}
setErrors(newErrors);
};
const handleSave = () => {
if (errors.size > 0) {
return;
}
const settings = SettingsService.getInstance();
const changedSettings: Record<string, any> = {};
for (const [key, value] of values.entries()) {
settings.set(key, value);
changedSettings[key] = value;
}
window.dispatchEvent(new CustomEvent('settings:changed', {
detail: changedSettings
}));
onClose();
};
const handleCancel = () => {
onClose();
};
const renderSettingInput = (setting: SettingDescriptor) => {
const value = values.get(setting.key) ?? setting.defaultValue;
const error = errors.get(setting.key);
switch (setting.type) {
case 'boolean':
return (
<div className="settings-field">
<label className="settings-label settings-label-checkbox">
<input
type="checkbox"
className="settings-checkbox"
checked={value}
onChange={(e) => handleValueChange(setting.key, e.target.checked, setting)}
/>
<span>{setting.label}</span>
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'number':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<input
type="number"
className={`settings-input ${error ? 'settings-input-error' : ''}`}
value={value}
onChange={(e) => handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)}
placeholder={setting.placeholder}
min={setting.min}
max={setting.max}
step={setting.step}
/>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'string':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<input
type="text"
className={`settings-input ${error ? 'settings-input-error' : ''}`}
value={value}
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
placeholder={setting.placeholder}
/>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'select':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<select
className={`settings-select ${error ? 'settings-input-error' : ''}`}
value={value}
onChange={(e) => {
const option = setting.options?.find(opt => String(opt.value) === e.target.value);
if (option) {
handleValueChange(setting.key, option.value, setting);
}
}}
>
{setting.options?.map((option) => (
<option key={String(option.value)} value={String(option.value)}>
{option.label}
</option>
))}
</select>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'range':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<div className="settings-range-wrapper">
<input
type="range"
className="settings-range"
value={value}
onChange={(e) => handleValueChange(setting.key, parseFloat(e.target.value), setting)}
min={setting.min}
max={setting.max}
step={setting.step}
/>
<span className="settings-range-value">{value}</span>
</div>
{error && <span className="settings-error">{error}</span>}
</div>
);
case 'color':
return (
<div className="settings-field">
<label className="settings-label">
{setting.label}
{setting.description && (
<span className="settings-hint">{setting.description}</span>
)}
</label>
<input
type="color"
className="settings-color-input"
value={value}
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
/>
{error && <span className="settings-error">{error}</span>}
</div>
);
default:
return null;
}
};
const selectedCategory = categories.find(c => c.id === selectedCategoryId);
return (
<div className="settings-overlay">
<div className="settings-window">
<div className="settings-header">
<div className="settings-title">
<SettingsIcon size={18} />
<h2></h2>
</div>
<button className="settings-close-btn" onClick={handleCancel}>
<X size={18} />
</button>
</div>
<div className="settings-body">
<div className="settings-sidebar">
{categories.map((category) => (
<button
key={category.id}
className={`settings-category-btn ${selectedCategoryId === category.id ? 'active' : ''}`}
onClick={() => setSelectedCategoryId(category.id)}
>
<span className="settings-category-title">{category.title}</span>
{category.description && (
<span className="settings-category-desc">{category.description}</span>
)}
<ChevronRight size={14} className="settings-category-arrow" />
</button>
))}
</div>
<div className="settings-content">
{selectedCategory && selectedCategory.sections.map((section) => (
<div key={section.id} className="settings-section">
<h3 className="settings-section-title">{section.title}</h3>
{section.description && (
<p className="settings-section-description">{section.description}</p>
)}
{section.settings.map((setting) => (
<div key={setting.key}>
{renderSettingInput(setting)}
</div>
))}
</div>
))}
{!selectedCategory && (
<div className="settings-empty">
<SettingsIcon size={48} />
<p></p>
</div>
)}
</div>
</div>
<div className="settings-footer">
<button className="settings-btn settings-btn-cancel" onClick={handleCancel}>
</button>
<button
className="settings-btn settings-btn-save"
onClick={handleSave}
disabled={errors.size > 0}
>
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import type { Core, ServiceContainer } from '@esengine/ecs-framework'; import type { Core, ServiceContainer } from '@esengine/ecs-framework';
import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub, PanelDescriptor } from '@esengine/editor-core'; import { IEditorPlugin, EditorPluginCategory, MenuItem, MessageHub, PanelDescriptor, SettingsRegistry } from '@esengine/editor-core';
import { ProfilerDockPanel } from '../components/ProfilerDockPanel'; import { ProfilerDockPanel } from '../components/ProfilerDockPanel';
import { ProfilerService } from '../services/ProfilerService'; import { ProfilerService } from '../services/ProfilerService';
@@ -22,6 +22,70 @@ export class ProfilerPlugin implements IEditorPlugin {
async install(_core: Core, services: ServiceContainer): Promise<void> { async install(_core: Core, services: ServiceContainer): Promise<void> {
this.messageHub = services.resolve(MessageHub); this.messageHub = services.resolve(MessageHub);
// 注册设置
const settingsRegistry = services.resolve(SettingsRegistry);
settingsRegistry.registerCategory({
id: 'profiler',
title: '性能分析器',
description: '配置性能分析器的行为和显示选项',
sections: [
{
id: 'connection',
title: '连接设置',
description: '配置WebSocket服务器连接参数',
settings: [
{
key: 'profiler.port',
label: '监听端口',
type: 'number',
defaultValue: 8080,
description: '性能分析器WebSocket服务器监听的端口号',
placeholder: '8080',
min: 1024,
max: 65535,
validator: {
validate: (value: number) => value >= 1024 && value <= 65535,
errorMessage: '端口号必须在1024到65535之间'
}
},
{
key: 'profiler.autoStart',
label: '自动启动服务器',
type: 'boolean',
defaultValue: true,
description: '编辑器启动时自动启动性能分析器服务器'
}
]
},
{
id: 'display',
title: '显示设置',
description: '配置性能数据的显示选项',
settings: [
{
key: 'profiler.refreshInterval',
label: '刷新间隔 (毫秒)',
type: 'range',
defaultValue: 100,
description: '性能数据刷新的时间间隔',
min: 50,
max: 1000,
step: 50
},
{
key: 'profiler.maxDataPoints',
label: '最大数据点数',
type: 'number',
defaultValue: 100,
description: '图表中保留的最大历史数据点数量',
min: 10,
max: 500
}
]
}
]
});
// 创建并启动 ProfilerService // 创建并启动 ProfilerService
this.profilerService = new ProfilerService(); this.profilerService = new ProfilerService();

View File

@@ -1,4 +1,5 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { SettingsService } from './SettingsService';
export interface SystemPerformanceData { export interface SystemPerformanceData {
name: string; name: string;
@@ -57,13 +58,45 @@ type ProfilerDataListener = (data: ProfilerData) => void;
export class ProfilerService { export class ProfilerService {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private isServerRunning = false; private isServerRunning = false;
private wsPort = '8080'; private wsPort: string;
private listeners: Set<ProfilerDataListener> = new Set(); private listeners: Set<ProfilerDataListener> = new Set();
private currentData: ProfilerData | null = null; private currentData: ProfilerData | null = null;
private checkServerInterval: NodeJS.Timeout | null = null; private checkServerInterval: NodeJS.Timeout | null = null;
private reconnectTimeout: NodeJS.Timeout | null = null; private reconnectTimeout: NodeJS.Timeout | null = null;
constructor() { constructor() {
const settings = SettingsService.getInstance();
this.wsPort = settings.get('profiler.port', '8080');
this.startServerCheck();
this.listenToSettingsChanges();
}
private listenToSettingsChanges(): void {
window.addEventListener('settings:changed', ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort && newPort !== this.wsPort) {
this.wsPort = newPort;
this.reconnectWithNewPort();
}
}) as EventListener);
}
private async reconnectWithNewPort(): Promise<void> {
this.disconnect();
if (this.checkServerInterval) {
clearInterval(this.checkServerInterval);
this.checkServerInterval = null;
}
try {
await invoke('stop_profiler_server');
this.isServerRunning = false;
} catch (error) {
console.error('[ProfilerService] Failed to stop server:', error);
}
this.startServerCheck(); this.startServerCheck();
} }

View File

@@ -0,0 +1,67 @@
export class SettingsService {
private static instance: SettingsService;
private settings: Map<string, any> = new Map();
private storageKey = 'editor-settings';
private constructor() {
this.loadSettings();
}
public static getInstance(): SettingsService {
if (!SettingsService.instance) {
SettingsService.instance = new SettingsService();
}
return SettingsService.instance;
}
private loadSettings(): void {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
const data = JSON.parse(stored);
this.settings = new Map(Object.entries(data));
}
} catch (error) {
console.error('[SettingsService] Failed to load settings:', error);
}
}
private saveSettings(): void {
try {
const data = Object.fromEntries(this.settings);
localStorage.setItem(this.storageKey, JSON.stringify(data));
} catch (error) {
console.error('[SettingsService] Failed to save settings:', error);
}
}
public get<T>(key: string, defaultValue: T): T {
if (this.settings.has(key)) {
return this.settings.get(key) as T;
}
return defaultValue;
}
public set<T>(key: string, value: T): void {
this.settings.set(key, value);
this.saveSettings();
}
public has(key: string): boolean {
return this.settings.has(key);
}
public delete(key: string): void {
this.settings.delete(key);
this.saveSettings();
}
public clear(): void {
this.settings.clear();
this.saveSettings();
}
public getAll(): Record<string, any> {
return Object.fromEntries(this.settings);
}
}

View File

@@ -0,0 +1,426 @@
.settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
backdrop-filter: blur(4px);
}
.settings-window {
background-color: var(--color-bg-base);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
width: 800px;
max-width: 90vw;
max-height: 80vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.settings-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-lg) var(--spacing-xl);
border-bottom: 1px solid var(--color-border-default);
background-color: var(--color-bg-elevated);
}
.settings-title {
display: flex;
align-items: center;
gap: var(--spacing-sm);
color: var(--color-text-primary);
}
.settings-title h2 {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
}
.settings-close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.settings-close-btn:hover {
background-color: var(--color-bg-hover);
color: var(--color-text-primary);
}
.settings-body {
flex: 1;
display: flex;
overflow: hidden;
}
.settings-sidebar {
width: 220px;
background-color: var(--color-bg-elevated);
border-right: 1px solid var(--color-border-default);
overflow-y: auto;
flex-shrink: 0;
}
.settings-sidebar::-webkit-scrollbar {
width: 8px;
}
.settings-sidebar::-webkit-scrollbar-track {
background: transparent;
}
.settings-sidebar::-webkit-scrollbar-thumb {
background: rgba(121, 121, 121, 0.3);
border-radius: 4px;
}
.settings-sidebar::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.5);
}
.settings-category-btn {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: var(--spacing-md) var(--spacing-lg);
background: none;
border: none;
border-left: 3px solid transparent;
cursor: pointer;
text-align: left;
transition: all var(--transition-fast);
position: relative;
}
.settings-category-btn:hover {
background-color: var(--color-bg-hover);
}
.settings-category-btn.active {
background-color: var(--color-bg-base);
border-left-color: var(--color-primary);
}
.settings-category-title {
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
}
.settings-category-desc {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
margin-top: 2px;
}
.settings-category-arrow {
position: absolute;
right: var(--spacing-md);
top: 50%;
transform: translateY(-50%);
color: var(--color-text-tertiary);
opacity: 0;
transition: opacity var(--transition-fast);
}
.settings-category-btn.active .settings-category-arrow {
opacity: 1;
}
.settings-content {
flex: 1;
overflow-y: auto;
padding: var(--spacing-xl);
}
.settings-content::-webkit-scrollbar {
width: 14px;
}
.settings-content::-webkit-scrollbar-track {
background: transparent;
}
.settings-content::-webkit-scrollbar-thumb {
background: rgba(121, 121, 121, 0.4);
border-radius: 8px;
border: 3px solid transparent;
background-clip: padding-box;
}
.settings-content::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.7);
background-clip: padding-box;
}
.settings-section {
margin-bottom: var(--spacing-2xl);
}
.settings-section:last-child {
margin-bottom: 0;
}
.settings-section-title {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin: 0 0 var(--spacing-md) 0;
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--color-border-subtle);
}
.settings-section-description {
font-size: var(--font-size-sm);
color: var(--color-text-secondary);
margin: var(--spacing-sm) 0 var(--spacing-md) 0;
}
.settings-field {
margin-bottom: var(--spacing-lg);
}
.settings-field:last-child {
margin-bottom: 0;
}
.settings-label {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin-bottom: var(--spacing-sm);
}
.settings-label-checkbox {
flex-direction: row;
align-items: flex-start;
gap: var(--spacing-sm);
margin-bottom: 0;
}
.settings-label-checkbox span:first-of-type {
font-weight: var(--font-weight-medium);
}
.settings-hint {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-normal);
color: var(--color-text-tertiary);
margin-top: 2px;
}
.settings-input {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-primary);
font-size: var(--font-size-sm);
font-family: var(--font-family-mono);
transition: all var(--transition-fast);
}
.settings-input:focus {
outline: none;
border-color: var(--color-primary);
background-color: var(--color-bg-base);
}
.settings-input:hover:not(:focus) {
border-color: var(--color-border-strong);
}
.settings-checkbox {
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
flex-shrink: 0;
}
.settings-footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-md);
padding: var(--spacing-lg) var(--spacing-xl);
border-top: 1px solid var(--color-border-default);
background-color: var(--color-bg-elevated);
}
.settings-btn {
padding: var(--spacing-sm) var(--spacing-lg);
border: none;
border-radius: var(--radius-sm);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
cursor: pointer;
transition: all var(--transition-fast);
}
.settings-btn-cancel {
background-color: var(--color-bg-hover);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);
}
.settings-btn-cancel:hover {
background-color: var(--color-bg-inset);
border-color: var(--color-border-strong);
}
.settings-btn-save {
background-color: var(--color-primary);
color: var(--color-text-inverse);
}
.settings-btn-save:hover {
background-color: var(--color-primary-hover);
}
.settings-btn-save:active {
transform: scale(0.98);
}
.settings-select {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-primary);
font-size: var(--font-size-sm);
cursor: pointer;
transition: all var(--transition-fast);
}
.settings-select:focus {
outline: none;
border-color: var(--color-primary);
background-color: var(--color-bg-base);
}
.settings-select:hover:not(:focus) {
border-color: var(--color-border-strong);
}
.settings-range-wrapper {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.settings-range {
flex: 1;
height: 6px;
border-radius: 3px;
background: var(--color-bg-inset);
outline: none;
-webkit-appearance: none;
}
.settings-range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-primary);
cursor: pointer;
transition: transform var(--transition-fast);
}
.settings-range::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.settings-range::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--color-primary);
cursor: pointer;
border: none;
transition: transform var(--transition-fast);
}
.settings-range::-moz-range-thumb:hover {
transform: scale(1.2);
}
.settings-range-value {
min-width: 40px;
text-align: center;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.settings-color-input {
width: 60px;
height: 40px;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
cursor: pointer;
transition: border-color var(--transition-fast);
}
.settings-color-input:hover {
border-color: var(--color-border-strong);
}
.settings-input-error {
border-color: var(--color-error) !important;
}
.settings-error {
display: block;
margin-top: var(--spacing-xs);
font-size: var(--font-size-xs);
color: var(--color-error);
}
.settings-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-tertiary);
text-align: center;
gap: var(--spacing-md);
}
.settings-empty svg {
opacity: 0.3;
}
.settings-empty p {
margin: 0;
font-size: var(--font-size-base);
}

View File

@@ -0,0 +1,192 @@
import { Injectable, IService } from '@esengine/ecs-framework';
export type SettingType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'range';
export interface SettingOption {
label: string;
value: any;
}
export interface SettingValidator {
validate: (value: any) => boolean;
errorMessage: string;
}
export interface SettingDescriptor {
key: string;
label: string;
type: SettingType;
defaultValue: any;
description?: string;
placeholder?: string;
options?: SettingOption[];
validator?: SettingValidator;
min?: number;
max?: number;
step?: number;
}
export interface SettingSection {
id: string;
title: string;
description?: string;
icon?: string;
settings: SettingDescriptor[];
}
export interface SettingCategory {
id: string;
title: string;
description?: string;
sections: SettingSection[];
}
@Injectable()
export class SettingsRegistry implements IService {
private categories: Map<string, SettingCategory> = new Map();
public dispose(): void {
this.categories.clear();
}
public registerCategory(category: SettingCategory): void {
if (this.categories.has(category.id)) {
console.warn(`[SettingsRegistry] Category ${category.id} already registered, overwriting`);
}
this.categories.set(category.id, category);
}
public registerSection(categoryId: string, section: SettingSection): void {
let category = this.categories.get(categoryId);
if (!category) {
category = {
id: categoryId,
title: categoryId,
sections: []
};
this.categories.set(categoryId, category);
}
const existingIndex = category.sections.findIndex(s => s.id === section.id);
if (existingIndex >= 0) {
category.sections[existingIndex] = section;
console.warn(`[SettingsRegistry] Section ${section.id} in category ${categoryId} already exists, overwriting`);
} else {
category.sections.push(section);
}
}
public registerSetting(categoryId: string, sectionId: string, setting: SettingDescriptor): void {
let category = this.categories.get(categoryId);
if (!category) {
category = {
id: categoryId,
title: categoryId,
sections: []
};
this.categories.set(categoryId, category);
}
let section = category.sections.find(s => s.id === sectionId);
if (!section) {
section = {
id: sectionId,
title: sectionId,
settings: []
};
category.sections.push(section);
}
const existingIndex = section.settings.findIndex(s => s.key === setting.key);
if (existingIndex >= 0) {
section.settings[existingIndex] = setting;
console.warn(`[SettingsRegistry] Setting ${setting.key} in section ${sectionId} already exists, overwriting`);
} else {
section.settings.push(setting);
}
}
public unregisterCategory(categoryId: string): void {
this.categories.delete(categoryId);
}
public unregisterSection(categoryId: string, sectionId: string): void {
const category = this.categories.get(categoryId);
if (category) {
category.sections = category.sections.filter(s => s.id !== sectionId);
if (category.sections.length === 0) {
this.categories.delete(categoryId);
}
}
}
public getCategory(categoryId: string): SettingCategory | undefined {
return this.categories.get(categoryId);
}
public getAllCategories(): SettingCategory[] {
return Array.from(this.categories.values());
}
public getSetting(categoryId: string, sectionId: string, key: string): SettingDescriptor | undefined {
const category = this.categories.get(categoryId);
if (!category) return undefined;
const section = category.sections.find(s => s.id === sectionId);
if (!section) return undefined;
return section.settings.find(s => s.key === key);
}
public getAllSettings(): Map<string, SettingDescriptor> {
const allSettings = new Map<string, SettingDescriptor>();
for (const category of this.categories.values()) {
for (const section of category.sections) {
for (const setting of section.settings) {
allSettings.set(setting.key, setting);
}
}
}
return allSettings;
}
public validateSetting(setting: SettingDescriptor, value: any): boolean {
if (setting.validator) {
return setting.validator.validate(value);
}
switch (setting.type) {
case 'number':
if (typeof value !== 'number') return false;
if (setting.min !== undefined && value < setting.min) return false;
if (setting.max !== undefined && value > setting.max) return false;
return true;
case 'boolean':
return typeof value === 'boolean';
case 'string':
return typeof value === 'string';
case 'select':
if (!setting.options) return false;
return setting.options.some(opt => opt.value === value);
case 'range':
if (typeof value !== 'number') return false;
if (setting.min !== undefined && value < setting.min) return false;
if (setting.max !== undefined && value > setting.max) return false;
return true;
case 'color':
return typeof value === 'string' && /^#[0-9A-Fa-f]{6}$/.test(value);
default:
return true;
}
}
}

View File

@@ -18,5 +18,6 @@ export * from './Services/ProjectService';
export * from './Services/ComponentDiscoveryService'; export * from './Services/ComponentDiscoveryService';
export * from './Services/ComponentLoaderService'; export * from './Services/ComponentLoaderService';
export * from './Services/LogService'; export * from './Services/LogService';
export * from './Services/SettingsRegistry';
export * from './Types/UITypes'; export * from './Types/UITypes';