设置界面
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
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 { ProfilerPlugin } from './plugins/ProfilerPlugin';
|
||||
import { StartupPage } from './components/StartupPage';
|
||||
@@ -12,6 +12,7 @@ import { ProfilerPanel } from './components/ProfilerPanel';
|
||||
import { PluginManagerWindow } from './components/PluginManagerWindow';
|
||||
import { ProfilerWindow } from './components/ProfilerWindow';
|
||||
import { PortManager } from './components/PortManager';
|
||||
import { SettingsWindow } from './components/SettingsWindow';
|
||||
import { Viewport } from './components/Viewport';
|
||||
import { MenuBar } from './components/MenuBar';
|
||||
import { DockContainer, DockablePanel } from './components/DockContainer';
|
||||
@@ -40,12 +41,14 @@ function App() {
|
||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||
const [logService, setLogService] = useState<LogService | null>(null);
|
||||
const [uiRegistry, setUiRegistry] = useState<UIRegistry | null>(null);
|
||||
const [settingsRegistry, setSettingsRegistry] = useState<SettingsRegistry | null>(null);
|
||||
const { t, locale, changeLocale } = useLocale();
|
||||
const [status, setStatus] = useState(t('header.status.initializing'));
|
||||
const [panels, setPanels] = useState<DockablePanel[]>([]);
|
||||
const [showPluginManager, setShowPluginManager] = useState(false);
|
||||
const [showProfiler, setShowProfiler] = useState(false);
|
||||
const [showPortManager, setShowPortManager] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0);
|
||||
const [isRemoteConnected, setIsRemoteConnected] = useState(false);
|
||||
|
||||
@@ -139,6 +142,7 @@ function App() {
|
||||
const componentLoader = new ComponentLoaderService(messageHub, componentRegistry);
|
||||
const propertyMetadata = new PropertyMetadataService();
|
||||
const logService = new LogService();
|
||||
const settingsRegistry = new SettingsRegistry();
|
||||
|
||||
Core.services.registerInstance(UIRegistry, uiRegistry);
|
||||
Core.services.registerInstance(MessageHub, messageHub);
|
||||
@@ -150,6 +154,7 @@ function App() {
|
||||
Core.services.registerInstance(ComponentLoaderService, componentLoader);
|
||||
Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
|
||||
Core.services.registerInstance(LogService, logService);
|
||||
Core.services.registerInstance(SettingsRegistry, settingsRegistry);
|
||||
|
||||
const pluginMgr = new EditorPluginManager();
|
||||
pluginMgr.initialize(coreInstance, Core.services);
|
||||
@@ -180,6 +185,7 @@ function App() {
|
||||
setMessageHub(messageHub);
|
||||
setLogService(logService);
|
||||
setUiRegistry(uiRegistry);
|
||||
setSettingsRegistry(settingsRegistry);
|
||||
setStatus(t('header.status.ready'));
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize editor:', error);
|
||||
@@ -426,6 +432,7 @@ function App() {
|
||||
onOpenPluginManager={() => setShowPluginManager(true)}
|
||||
onOpenProfiler={() => setShowProfiler(true)}
|
||||
onOpenPortManager={() => setShowPortManager(true)}
|
||||
onOpenSettings={() => setShowSettings(true)}
|
||||
onToggleDevtools={handleToggleDevtools}
|
||||
/>
|
||||
<div className="header-right">
|
||||
@@ -460,6 +467,10 @@ function App() {
|
||||
{showPortManager && (
|
||||
<PortManager onClose={() => setShowPortManager(false)} />
|
||||
)}
|
||||
|
||||
{showSettings && settingsRegistry && (
|
||||
<SettingsWindow onClose={() => setShowSettings(false)} settingsRegistry={settingsRegistry} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ interface MenuBarProps {
|
||||
onOpenPluginManager?: () => void;
|
||||
onOpenProfiler?: () => void;
|
||||
onOpenPortManager?: () => void;
|
||||
onOpenSettings?: () => void;
|
||||
onToggleDevtools?: () => void;
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ export function MenuBar({
|
||||
onOpenPluginManager,
|
||||
onOpenProfiler,
|
||||
onOpenPortManager,
|
||||
onOpenSettings,
|
||||
onToggleDevtools
|
||||
}: MenuBarProps) {
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||
@@ -139,6 +141,7 @@ export function MenuBar({
|
||||
pluginManager: 'Plugin Manager',
|
||||
tools: 'Tools',
|
||||
portManager: 'Port Manager',
|
||||
settings: 'Settings',
|
||||
help: 'Help',
|
||||
documentation: 'Documentation',
|
||||
about: 'About',
|
||||
@@ -170,6 +173,7 @@ export function MenuBar({
|
||||
pluginManager: '插件管理器',
|
||||
tools: '工具',
|
||||
portManager: '端口管理器',
|
||||
settings: '设置',
|
||||
help: '帮助',
|
||||
documentation: '文档',
|
||||
about: '关于',
|
||||
@@ -222,7 +226,9 @@ export function MenuBar({
|
||||
{ label: t('devtools'), onClick: onToggleDevtools }
|
||||
],
|
||||
tools: [
|
||||
{ label: t('portManager'), onClick: onOpenPortManager }
|
||||
{ label: t('portManager'), onClick: onOpenPortManager },
|
||||
{ separator: true },
|
||||
{ label: t('settings'), onClick: onOpenSettings }
|
||||
],
|
||||
help: [
|
||||
{ label: t('documentation'), disabled: true },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2 } from 'lucide-react';
|
||||
import { ProfilerService, ProfilerData } from '../services/ProfilerService';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import '../styles/ProfilerDockPanel.css';
|
||||
@@ -9,6 +10,25 @@ export function ProfilerDockPanel() {
|
||||
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
|
||||
const [isConnected, setIsConnected] = 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(() => {
|
||||
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
|
||||
@@ -99,7 +119,7 @@ export function ProfilerDockPanel() {
|
||||
<div className="profiler-dock-empty">
|
||||
<Activity size={32} />
|
||||
<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 className="profiler-dock-content">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
||||
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 { ProfilerService } from '../services/ProfilerService';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import '../styles/ProfilerWindow.css';
|
||||
|
||||
interface SystemPerformanceData {
|
||||
@@ -33,8 +34,27 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isServerRunning, setIsServerRunning] = useState(false);
|
||||
const [port, setPort] = useState('8080');
|
||||
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
|
||||
useEffect(() => {
|
||||
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="connection-port-display">
|
||||
<Server size={14} />
|
||||
<span>Port: 8080</span>
|
||||
<span>ws://localhost:{port}</span>
|
||||
</div>
|
||||
{isConnected ? (
|
||||
<div className="connection-status-indicator connected">
|
||||
|
||||
295
packages/editor-app/src/components/SettingsWindow.tsx
Normal file
295
packages/editor-app/src/components/SettingsWindow.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { ProfilerService } from '../services/ProfilerService';
|
||||
|
||||
@@ -22,6 +22,70 @@ export class ProfilerPlugin implements IEditorPlugin {
|
||||
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
||||
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
|
||||
this.profilerService = new ProfilerService();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { SettingsService } from './SettingsService';
|
||||
|
||||
export interface SystemPerformanceData {
|
||||
name: string;
|
||||
@@ -57,13 +58,45 @@ type ProfilerDataListener = (data: ProfilerData) => void;
|
||||
export class ProfilerService {
|
||||
private ws: WebSocket | null = null;
|
||||
private isServerRunning = false;
|
||||
private wsPort = '8080';
|
||||
private wsPort: string;
|
||||
private listeners: Set<ProfilerDataListener> = new Set();
|
||||
private currentData: ProfilerData | null = null;
|
||||
private checkServerInterval: NodeJS.Timeout | null = null;
|
||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
67
packages/editor-app/src/services/SettingsService.ts
Normal file
67
packages/editor-app/src/services/SettingsService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
426
packages/editor-app/src/styles/SettingsWindow.css
Normal file
426
packages/editor-app/src/styles/SettingsWindow.css
Normal 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);
|
||||
}
|
||||
192
packages/editor-core/src/Services/SettingsRegistry.ts
Normal file
192
packages/editor-core/src/Services/SettingsRegistry.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,6 @@ export * from './Services/ProjectService';
|
||||
export * from './Services/ComponentDiscoveryService';
|
||||
export * from './Services/ComponentLoaderService';
|
||||
export * from './Services/LogService';
|
||||
export * from './Services/SettingsRegistry';
|
||||
|
||||
export * from './Types/UITypes';
|
||||
|
||||
Reference in New Issue
Block a user