2025-10-16 13:07:19 +08:00
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
|
import { X, Settings as SettingsIcon, ChevronRight } from 'lucide-react';
|
2025-11-27 20:42:46 +08:00
|
|
|
|
import { Core } from '@esengine/ecs-framework';
|
2025-10-16 13:07:19 +08:00
|
|
|
|
import { SettingsService } from '../services/SettingsService';
|
2025-11-27 20:42:46 +08:00
|
|
|
|
import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager } from '@esengine/editor-core';
|
|
|
|
|
|
import { PluginListSetting } from './PluginListSetting';
|
2025-10-16 13:07:19 +08:00
|
|
|
|
import '../styles/SettingsWindow.css';
|
|
|
|
|
|
|
|
|
|
|
|
interface SettingsWindowProps {
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
settingsRegistry: SettingsRegistry;
|
2025-11-27 20:42:46 +08:00
|
|
|
|
initialCategoryId?: string;
|
2025-10-16 13:07:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: SettingsWindowProps) {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const [categories, setCategories] = useState<SettingCategory[]>([]);
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(initialCategoryId || null);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const [values, setValues] = useState<Map<string, any>>(new Map());
|
|
|
|
|
|
const [errors, setErrors] = useState<Map<string, string>>(new Map());
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const allCategories = settingsRegistry.getAllCategories();
|
|
|
|
|
|
setCategories(allCategories);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
if (allCategories.length > 0 && !selectedCategoryId) {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// 如果有 initialCategoryId,尝试使用它
|
|
|
|
|
|
if (initialCategoryId && allCategories.some(c => c.id === initialCategoryId)) {
|
|
|
|
|
|
setSelectedCategoryId(initialCategoryId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const firstCategory = allCategories[0];
|
|
|
|
|
|
if (firstCategory) {
|
|
|
|
|
|
setSelectedCategoryId(firstCategory.id);
|
|
|
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const settings = SettingsService.getInstance();
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const projectService = Core.services.tryResolve<ProjectService>(ProjectService);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const allSettings = settingsRegistry.getAllSettings();
|
|
|
|
|
|
const initialValues = new Map<string, any>();
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
for (const [key, descriptor] of allSettings.entries()) {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// Project-scoped settings are loaded from ProjectService
|
|
|
|
|
|
if (key.startsWith('project.') && projectService) {
|
|
|
|
|
|
if (key === 'project.uiDesignResolution.width') {
|
|
|
|
|
|
const resolution = projectService.getUIDesignResolution();
|
|
|
|
|
|
initialValues.set(key, resolution.width);
|
|
|
|
|
|
} else if (key === 'project.uiDesignResolution.height') {
|
|
|
|
|
|
const resolution = projectService.getUIDesignResolution();
|
|
|
|
|
|
initialValues.set(key, resolution.height);
|
|
|
|
|
|
} else if (key === 'project.uiDesignResolution.preset') {
|
|
|
|
|
|
const resolution = projectService.getUIDesignResolution();
|
|
|
|
|
|
initialValues.set(key, `${resolution.width}x${resolution.height}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// For other project settings, use default
|
|
|
|
|
|
initialValues.set(key, descriptor.defaultValue);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const value = settings.get(key, descriptor.defaultValue);
|
|
|
|
|
|
initialValues.set(key, value);
|
|
|
|
|
|
}
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
setValues(initialValues);
|
|
|
|
|
|
}, [settingsRegistry, selectedCategoryId]);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => {
|
|
|
|
|
|
const newValues = new Map(values);
|
|
|
|
|
|
newValues.set(key, value);
|
|
|
|
|
|
setValues(newValues);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const newErrors = new Map(errors);
|
|
|
|
|
|
if (!settingsRegistry.validateSetting(descriptor, value)) {
|
|
|
|
|
|
newErrors.set(key, descriptor.validator?.errorMessage || 'Invalid value');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newErrors.delete(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
setErrors(newErrors);
|
|
|
|
|
|
};
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const handleSave = async () => {
|
2025-11-02 23:50:41 +08:00
|
|
|
|
if (errors.size > 0) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const settings = SettingsService.getInstance();
|
2025-11-27 20:42:46 +08:00
|
|
|
|
const projectService = Core.services.tryResolve<ProjectService>(ProjectService);
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const changedSettings: Record<string, any> = {};
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// Track UI resolution changes for batch saving
|
|
|
|
|
|
let uiResolutionChanged = false;
|
|
|
|
|
|
let newWidth = 1920;
|
|
|
|
|
|
let newHeight = 1080;
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
for (const [key, value] of values.entries()) {
|
2025-11-27 20:42:46 +08:00
|
|
|
|
// Project-scoped settings are saved to ProjectService
|
|
|
|
|
|
if (key.startsWith('project.') && projectService) {
|
|
|
|
|
|
if (key === 'project.uiDesignResolution.width') {
|
|
|
|
|
|
newWidth = value;
|
|
|
|
|
|
uiResolutionChanged = true;
|
|
|
|
|
|
} else if (key === 'project.uiDesignResolution.height') {
|
|
|
|
|
|
newHeight = value;
|
|
|
|
|
|
uiResolutionChanged = true;
|
|
|
|
|
|
} else if (key === 'project.uiDesignResolution.preset') {
|
|
|
|
|
|
// Preset changes width and height together
|
|
|
|
|
|
const [w, h] = value.split('x').map(Number);
|
|
|
|
|
|
if (w && h) {
|
|
|
|
|
|
newWidth = w;
|
|
|
|
|
|
newHeight = h;
|
|
|
|
|
|
uiResolutionChanged = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
changedSettings[key] = value;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
settings.set(key, value);
|
|
|
|
|
|
changedSettings[key] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Save UI resolution if changed
|
|
|
|
|
|
if (uiResolutionChanged && projectService) {
|
|
|
|
|
|
await projectService.setUIDesignResolution({ width: newWidth, height: newHeight });
|
2025-11-02 23:50:41 +08:00
|
|
|
|
}
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
window.dispatchEvent(new CustomEvent('settings:changed', {
|
|
|
|
|
|
detail: changedSettings
|
|
|
|
|
|
}));
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
onClose();
|
|
|
|
|
|
};
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
};
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const renderSettingInput = (setting: SettingDescriptor) => {
|
|
|
|
|
|
const value = values.get(setting.key) ?? setting.defaultValue;
|
|
|
|
|
|
const error = errors.get(setting.key);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
|
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-27 20:42:46 +08:00
|
|
|
|
case 'pluginList': {
|
|
|
|
|
|
const pluginManager = Core.services.tryResolve<PluginManager>(IPluginManager);
|
|
|
|
|
|
if (!pluginManager) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="settings-field settings-field-full">
|
|
|
|
|
|
<p className="settings-error">PluginManager 不可用</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="settings-field settings-field-full">
|
|
|
|
|
|
<PluginListSetting pluginManager={pluginManager} />
|
|
|
|
|
|
{error && <span className="settings-error">{error}</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
default:
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
const selectedCategory = categories.find((c) => c.id === selectedCategoryId);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
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>
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<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>
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<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>
|
|
|
|
|
|
))}
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
{!selectedCategory && (
|
|
|
|
|
|
<div className="settings-empty">
|
|
|
|
|
|
<SettingsIcon size={48} />
|
|
|
|
|
|
<p>请选择一个设置分类</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-16 13:07:19 +08:00
|
|
|
|
|
2025-11-02 23:50:41 +08:00
|
|
|
|
<div className="settings-footer">
|
|
|
|
|
|
<button className="settings-btn settings-btn-cancel" onClick={handleCancel}>
|
2025-10-16 13:07:19 +08:00
|
|
|
|
取消
|
2025-11-02 23:50:41 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="settings-btn settings-btn-save"
|
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
|
disabled={errors.size > 0}
|
|
|
|
|
|
>
|
2025-10-16 13:07:19 +08:00
|
|
|
|
保存
|
2025-11-02 23:50:41 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-10-16 13:07:19 +08:00
|
|
|
|
</div>
|
2025-11-02 23:50:41 +08:00
|
|
|
|
);
|
2025-10-16 13:07:19 +08:00
|
|
|
|
}
|