Files
MaliangAINovalWriter/AINoval/lib/screens/settings/widgets/editor_settings_panel.dart
2025-09-10 00:07:52 +08:00

853 lines
34 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:ainoval/models/editor_settings.dart';
// import 'package:ainoval/widgets/common/settings_widgets.dart';
import 'package:ainoval/utils/web_theme.dart';
/// 编辑器设置面板 - 紧凑版
/// 提供完整的编辑器配置选项,优化为一页显示
class EditorSettingsPanel extends StatefulWidget {
const EditorSettingsPanel({
super.key,
required this.settings,
required this.onSettingsChanged,
this.onSave,
this.onReset,
});
final EditorSettings settings;
final ValueChanged<EditorSettings> onSettingsChanged;
final VoidCallback? onSave;
final VoidCallback? onReset;
@override
State<EditorSettingsPanel> createState() => _EditorSettingsPanelState();
}
class _EditorSettingsPanelState extends State<EditorSettingsPanel> {
late EditorSettings _currentSettings;
bool _hasUnsavedChanges = false;
bool _isSaving = false;
@override
void initState() {
super.initState();
_currentSettings = widget.settings;
}
@override
void didUpdateWidget(EditorSettingsPanel oldWidget) {
super.didUpdateWidget(oldWidget);
// 🚀 修复:只有当外部设置真正改变且不是用户操作导致的时,才重置状态
if (oldWidget.settings != widget.settings) {
// 如果当前设置与新的widget设置相同说明设置已被外部保存
if (_currentSettings == widget.settings) {
setState(() {
_hasUnsavedChanges = false;
});
} else {
// 如果不同,更新基础设置但保持未保存状态
setState(() {
_currentSettings = widget.settings;
_hasUnsavedChanges = false;
});
}
}
}
void _updateSettings(EditorSettings newSettings) {
setState(() {
_currentSettings = newSettings;
// 🚀 修复保存按钮逻辑:先设置未保存状态,再调用回调
_hasUnsavedChanges = true;
});
// 通知父组件设置已更改(用于实时预览),但不影响保存状态
widget.onSettingsChanged(newSettings);
}
Future<void> _handleSave() async {
if (_isSaving) return; // 🚀 简化:只检查是否正在保存
setState(() {
_isSaving = true;
});
try {
// 🚀 实际调用保存回调
widget.onSave?.call();
// 等待一小段时间确保保存操作完成
await Future.delayed(const Duration(milliseconds: 300));
setState(() {
_hasUnsavedChanges = false;
});
// 显示保存成功提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('编辑器设置已保存'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('保存失败: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
} finally {
if (mounted) {
setState(() {
_isSaving = false;
});
}
}
}
void _handleReset() {
setState(() {
_currentSettings = const EditorSettings();
_hasUnsavedChanges = true;
});
widget.onSettingsChanged(_currentSettings);
widget.onReset?.call();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 固定顶部:标题和操作按钮
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: WebTheme.getBackgroundColor(context),
border: Border(
bottom: BorderSide(color: WebTheme.grey200, width: 1),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 标题行
Row(
children: [
Icon(Icons.edit_note, size: 24, color: WebTheme.getTextColor(context)),
const SizedBox(width: 8),
Text(
'编辑器设置',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: WebTheme.getTextColor(context),
fontWeight: FontWeight.w600,
),
),
const Spacer(),
// 保存状态指示
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: (_hasUnsavedChanges
? WebTheme.getPrimaryColor(context)
: WebTheme.getSecondaryTextColor(context))
.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: (_hasUnsavedChanges
? WebTheme.getPrimaryColor(context)
: WebTheme.getSecondaryTextColor(context))
.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_hasUnsavedChanges ? Icons.settings : Icons.check_circle,
size: 12,
color: _hasUnsavedChanges
? WebTheme.getPrimaryColor(context)
: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(width: 4),
Text(
_hasUnsavedChanges ? '可保存' : '已保存',
style: TextStyle(
fontSize: 12,
color: _hasUnsavedChanges
? WebTheme.getPrimaryColor(context)
: WebTheme.getSecondaryTextColor(context),
),
),
],
),
),
],
),
const SizedBox(height: 8),
// 操作按钮行
Row(
children: [
Text(
'自定义编辑器外观和行为',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: WebTheme.getSecondaryTextColor(context),
),
),
const Spacer(),
// 重置按钮
TextButton.icon(
onPressed: _handleReset,
icon: const Icon(Icons.refresh, size: 16),
label: const Text('重置'),
style: TextButton.styleFrom(
foregroundColor: WebTheme.getSecondaryTextColor(context),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
),
const SizedBox(width: 8),
// 保存按钮 - 🚀 修改为一直可点击
ElevatedButton.icon(
onPressed: !_isSaving ? _handleSave : null,
icon: _isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(Icons.save, size: 16),
label: Text(_isSaving ? '保存中...' : '保存设置'),
style: ElevatedButton.styleFrom(
backgroundColor: WebTheme.getPrimaryColor(context),
foregroundColor: WebTheme.white,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 2,
),
),
],
),
],
),
),
// 可滚动的设置内容
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 紧凑的双列布局
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左列
Expanded(
child: Column(
children: [
_buildCompactCard(
title: '字体设置',
icon: Icons.text_fields,
children: [
_buildCompactSlider(
'字体大小',
_currentSettings.fontSize,
12, 32, '像素',
(value) => _updateSettings(_currentSettings.copyWith(fontSize: value)),
),
_buildCompactDropdown(
'字体',
_currentSettings.fontFamily,
EditorSettings.availableFontFamilies,
(value) => _updateSettings(_currentSettings.copyWith(fontFamily: value)),
itemBuilder: (font) {
switch (font) {
case 'Roboto': return 'Roboto英文推荐';
case 'serif': return '衬线字体(中文推荐)';
case 'sans-serif': return '无衬线字体(中文推荐)';
case 'monospace': return '等宽字体';
case 'Noto Sans SC': return 'Noto Sans SC思源黑体';
case 'PingFang SC': return 'PingFang SC苹方';
case 'Microsoft YaHei': return 'Microsoft YaHei微软雅黑';
case 'SimHei': return 'SimHei黑体';
case 'SimSun': return 'SimSun宋体';
case 'Times New Roman': return 'Times New Roman英文衬线';
case 'Arial': return 'Arial英文无衬线';
default: return font;
}
},
),
_buildCompactDropdown(
'字体粗细',
_currentSettings.fontWeight,
EditorSettings.availableFontWeights,
(value) => _updateSettings(_currentSettings.copyWith(fontWeight: value)),
itemBuilder: (weight) {
switch (weight) {
case FontWeight.w300: return '细体 (300)';
case FontWeight.w400: return '正常 (400)';
case FontWeight.w500: return '中等 (500)';
case FontWeight.w600: return '半粗 (600)';
case FontWeight.w700: return '粗体 (700)';
default: return '正常 (400)';
}
},
),
_buildCompactSlider(
'行间距',
_currentSettings.lineSpacing,
1.0, 3.0, '',
(value) => _updateSettings(_currentSettings.copyWith(lineSpacing: value)),
formatValue: (value) => '${value.toStringAsFixed(1)}x',
),
_buildCompactSlider(
'字符间距',
_currentSettings.letterSpacing,
-1.0, 2.0, '像素', // 🚀 缩小调整范围,更适合中文
(value) => _updateSettings(_currentSettings.copyWith(letterSpacing: value)),
formatValue: (value) => value == 0
? '标准'
: (value > 0 ? '+${value.toStringAsFixed(1)}px' : '${value.toStringAsFixed(1)}px'),
),
],
),
const SizedBox(height: 10),
_buildCompactCard(
title: '编辑器行为',
icon: Icons.settings,
children: [
_buildCompactSwitch('自动保存', _currentSettings.autoSaveEnabled,
(value) => _updateSettings(_currentSettings.copyWith(autoSaveEnabled: value))),
if (_currentSettings.autoSaveEnabled)
_buildCompactSlider(
'保存间隔',
_currentSettings.autoSaveIntervalMinutes.toDouble(),
1, 15, '分钟',
(value) => _updateSettings(_currentSettings.copyWith(autoSaveIntervalMinutes: value.round())),
formatValue: (value) => '${value.toInt()}分钟',
),
_buildCompactSwitch('拼写检查', _currentSettings.spellCheckEnabled,
(value) => _updateSettings(_currentSettings.copyWith(spellCheckEnabled: value))),
_buildCompactSwitch('显示字数', _currentSettings.showWordCount,
(value) => _updateSettings(_currentSettings.copyWith(showWordCount: value))),
_buildCompactSwitch('显示行号', _currentSettings.showLineNumbers,
(value) => _updateSettings(_currentSettings.copyWith(showLineNumbers: value))),
_buildCompactSwitch('高亮当前行', _currentSettings.highlightActiveLine,
(value) => _updateSettings(_currentSettings.copyWith(highlightActiveLine: value))),
_buildCompactSwitch('Vim模式', _currentSettings.enableVimMode,
(value) => _updateSettings(_currentSettings.copyWith(enableVimMode: value))),
],
),
const SizedBox(height: 10),
// 🚀 移动导出设置到左列
_buildCompactCard(
title: '导出设置',
icon: Icons.download,
children: [
_buildCompactDropdown(
'默认导出格式',
_currentSettings.defaultExportFormat,
EditorSettings.availableExportFormats,
(value) => _updateSettings(_currentSettings.copyWith(defaultExportFormat: value)),
itemBuilder: (format) {
switch (format) {
case 'markdown': return 'Markdown (.md)';
case 'docx': return 'Word文档 (.docx)';
case 'pdf': return 'PDF文档 (.pdf)';
case 'txt': return '纯文本 (.txt)';
case 'html': return 'HTML文档 (.html)';
default: return format.toUpperCase();
}
},
),
_buildCompactSwitch('包含元数据', _currentSettings.includeMetadata,
(value) => _updateSettings(_currentSettings.copyWith(includeMetadata: value))),
],
),
],
),
),
const SizedBox(width: 16),
// 右列
Expanded(
child: Column(
children: [
_buildCompactCard(
title: '布局间距',
icon: Icons.format_align_center,
children: [
_buildCompactSlider(
'水平边距',
_currentSettings.paddingHorizontal,
8, 48, '像素',
(value) => _updateSettings(_currentSettings.copyWith(paddingHorizontal: value)),
),
_buildCompactSlider(
'垂直边距',
_currentSettings.paddingVertical,
8, 32, '像素',
(value) => _updateSettings(_currentSettings.copyWith(paddingVertical: value)),
),
_buildCompactSlider(
'段落间距',
_currentSettings.paragraphSpacing,
4, 24, '像素',
(value) => _updateSettings(_currentSettings.copyWith(paragraphSpacing: value)),
),
_buildCompactSlider(
'缩进大小',
_currentSettings.indentSize,
16, 64, '像素',
(value) => _updateSettings(_currentSettings.copyWith(indentSize: value)),
),
_buildCompactSlider(
'最大行宽',
_currentSettings.maxLineWidth,
400, 1500, '像素',
(value) => _updateSettings(_currentSettings.copyWith(maxLineWidth: value)),
),
_buildCompactSlider(
'最小编辑器高度',
_currentSettings.minEditorHeight,
1200, 3000, '像素',
(value) => _updateSettings(_currentSettings.copyWith(minEditorHeight: value)),
),
],
),
const SizedBox(height: 10),
_buildCompactCard(
title: '视觉效果',
icon: Icons.visibility,
children: [
_buildCompactSwitch('暗色模式', _currentSettings.darkModeEnabled,
(value) => _updateSettings(_currentSettings.copyWith(darkModeEnabled: value))),
_buildCompactSwitch('平滑滚动', _currentSettings.smoothScrolling,
(value) => _updateSettings(_currentSettings.copyWith(smoothScrolling: value))),
_buildCompactSwitch('淡入动画', _currentSettings.fadeInAnimation,
(value) => _updateSettings(_currentSettings.copyWith(fadeInAnimation: value))),
_buildCompactSwitch('打字机模式', _currentSettings.useTypewriterMode,
(value) => _updateSettings(_currentSettings.copyWith(useTypewriterMode: value))),
_buildCompactSwitch('显示小地图', _currentSettings.showMiniMap,
(value) => _updateSettings(_currentSettings.copyWith(showMiniMap: value))),
_buildCompactSlider(
'光标闪烁速度',
_currentSettings.cursorBlinkRate,
0.5, 3.0, '',
(value) => _updateSettings(_currentSettings.copyWith(cursorBlinkRate: value)),
formatValue: (value) => '${value.toStringAsFixed(1)}s',
),
],
),
const SizedBox(height: 10),
// 🚀 保留选择和光标设置卡片在右列
_buildCompactCard(
title: '选择和光标',
icon: Icons.colorize,
children: [
_buildColorPicker(
'选择高亮颜色',
Color(_currentSettings.selectionHighlightColor),
(color) => _updateSettings(_currentSettings.copyWith(selectionHighlightColor: color.value)),
),
],
),
],
),
),
],
),
const SizedBox(height: 16),
// 预览区域
_buildPreviewCard(),
],
),
),
),
],
);
}
Widget _buildCompactCard({
required String title,
required IconData icon,
required List<Widget> children,
}) {
return Container(
decoration: BoxDecoration(
color: WebTheme.getSurfaceColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: WebTheme.grey200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 卡片标题 - 🚀 减少内边距
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: WebTheme.grey50,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
Icon(icon, size: 16, color: WebTheme.getTextColor(context)),
const SizedBox(width: 6),
Text(
title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
),
),
],
),
),
// 卡片内容 - 🚀 减少内边距
Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: children,
),
),
],
),
);
}
Widget _buildCompactSlider(
String label,
double value,
double min,
double max,
String unit,
ValueChanged<double> onChanged, {
String Function(double)? formatValue,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
Text(
formatValue?.call(value) ?? '${value.toStringAsFixed(value % 1 == 0 ? 0 : 1)}$unit',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: WebTheme.getSecondaryTextColor(context),
),
),
],
),
SizedBox(
height: 26,
child: Slider(
value: value.clamp(min, max).toDouble(),
min: min,
max: max,
divisions: ((max - min) * (unit == '' ? 10 : 1)).round(),
onChanged: onChanged,
activeColor: WebTheme.getPrimaryColor(context),
inactiveColor: WebTheme.grey300,
),
),
],
),
);
}
Widget _buildCompactSwitch(
String label,
bool value,
ValueChanged<bool> onChanged,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, // 🚀 对齐优化
children: [
Expanded( // 🚀 让文字可以自动换行
child: Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8), // 🚀 添加间距
// 🚀 优化开关大小,与文字高度匹配
Transform.scale(
scale: 0.8, // 缩小开关
child: Switch(
value: value,
onChanged: onChanged,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
activeColor: WebTheme.getPrimaryColor(context),
inactiveThumbColor: WebTheme.grey400,
inactiveTrackColor: Colors.grey[300],
),
),
],
),
);
}
Widget _buildCompactDropdown<T>(
String label,
T value,
List<T> items,
ValueChanged<T?> onChanged, {
String Function(T)? itemBuilder,
}) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 3),
SizedBox(
height: 30,
child: DropdownButtonFormField<T>(
value: value,
items: items.map((item) {
return DropdownMenuItem<T>(
value: item,
child: Text(
itemBuilder?.call(item) ?? item.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
);
}).toList(),
onChanged: onChanged,
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: WebTheme.grey300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide(color: WebTheme.grey300),
),
),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
);
}
/// 🚀 构建颜色选择器
Widget _buildColorPicker(
String label,
Color currentColor,
ValueChanged<Color> onColorChanged,
) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 3),
GestureDetector(
onTap: () => _showColorPicker(currentColor, onColorChanged),
child: Container(
height: 30,
width: double.infinity,
decoration: BoxDecoration(
color: currentColor,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: WebTheme.grey300),
),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: currentColor,
borderRadius: const BorderRadius.horizontal(left: Radius.circular(4)),
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: WebTheme.getSurfaceColor(context),
borderRadius: const BorderRadius.horizontal(right: Radius.circular(4)),
),
child: Text(
'#${currentColor.value.toRadixString(16).substring(2).toUpperCase()}',
style: Theme.of(context).textTheme.bodySmall,
),
),
],
),
),
),
],
),
);
}
/// 显示颜色选择对话框
void _showColorPicker(Color currentColor, ValueChanged<Color> onColorChanged) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('选择颜色'),
content: SizedBox(
width: 300,
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
Colors.red,
Colors.pink,
Colors.purple,
Colors.deepPurple,
Colors.indigo,
Colors.blue,
Colors.lightBlue,
Colors.cyan,
Colors.teal,
Colors.green,
Colors.lightGreen,
Colors.lime,
Colors.yellow,
Colors.amber,
Colors.orange,
Colors.deepOrange,
Colors.brown,
Colors.grey,
Colors.blueGrey,
Colors.black,
].map((color) => GestureDetector(
onTap: () {
onColorChanged(color);
Navigator.of(context).pop();
},
child: Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: currentColor == color ? Colors.white : Colors.transparent,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 2,
offset: const Offset(0, 1),
),
],
),
),
)).toList(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
],
),
);
}
Widget _buildPreviewCard() {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: WebTheme.getSurfaceColor(context),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: WebTheme.grey200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: WebTheme.grey50,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
child: Row(
children: [
Icon(Icons.preview, size: 18, color: WebTheme.getTextColor(context)),
const SizedBox(width: 8),
Text(
'预览效果',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
),
),
],
),
),
Container(
width: double.infinity,
constraints: const BoxConstraints(maxWidth: 800),
padding: EdgeInsets.symmetric(
horizontal: _currentSettings.paddingHorizontal,
vertical: _currentSettings.paddingVertical,
),
child: Text(
'这是预览文本,展示当前字体设置的效果。您可以看到字体大小、行间距、字体样式等设置的实际显示效果。',
style: TextStyle(
fontFamily: _currentSettings.fontFamily,
fontSize: _currentSettings.fontSize,
fontWeight: _currentSettings.fontWeight,
height: _currentSettings.lineSpacing,
letterSpacing: _currentSettings.letterSpacing,
color: WebTheme.getTextColor(context),
),
),
),
],
),
);
}
}