马良AI写作初始化仓库
This commit is contained in:
150
AINoval/lib/screens/editor/managers/editor_dialog_manager.dart
Normal file
150
AINoval/lib/screens/editor/managers/editor_dialog_manager.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 编辑器对话框管理器
|
||||
/// 负责管理编辑器中的各种对话框
|
||||
class EditorDialogManager {
|
||||
// 显示编辑器侧边栏宽度调整对话框
|
||||
static void showEditorSidebarWidthDialog(
|
||||
BuildContext context,
|
||||
double currentWidth,
|
||||
double minWidth,
|
||||
double maxWidth,
|
||||
ValueChanged<double> onWidthChanged,
|
||||
VoidCallback onSave,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return _buildWidthAdjustmentDialog(
|
||||
context,
|
||||
'调整侧边栏宽度',
|
||||
currentWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
onWidthChanged,
|
||||
onSave,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 显示聊天侧边栏宽度调整对话框
|
||||
static void showChatSidebarWidthDialog(
|
||||
BuildContext context,
|
||||
double currentWidth,
|
||||
double minWidth,
|
||||
double maxWidth,
|
||||
ValueChanged<double> onWidthChanged,
|
||||
VoidCallback onSave,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return _buildWidthAdjustmentDialog(
|
||||
context,
|
||||
'调整聊天侧边栏宽度',
|
||||
currentWidth,
|
||||
minWidth,
|
||||
maxWidth,
|
||||
onWidthChanged,
|
||||
onSave,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 构建宽度调整对话框
|
||||
static Widget _buildWidthAdjustmentDialog(
|
||||
BuildContext context,
|
||||
String title,
|
||||
double currentWidth,
|
||||
double minWidth,
|
||||
double maxWidth,
|
||||
ValueChanged<double> onWidthChanged,
|
||||
VoidCallback onSave,
|
||||
) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('当前宽度: ${currentWidth.toInt()} 像素'),
|
||||
const SizedBox(height: 16),
|
||||
StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return Slider(
|
||||
value: currentWidth,
|
||||
min: minWidth,
|
||||
max: maxWidth,
|
||||
divisions: 8,
|
||||
label: currentWidth.toInt().toString(),
|
||||
onChanged: (value) {
|
||||
onWidthChanged(value);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
onSave();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 显示登录提示对话框
|
||||
static Widget buildLoginRequiredPanel(BuildContext context, VoidCallback onClose) {
|
||||
return Material(
|
||||
elevation: 4.0,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
child: Container(
|
||||
width: 400, // Smaller width for message
|
||||
height: 200, // Smaller height for message
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.lock_outline,
|
||||
size: 40, color: Theme.of(context).colorScheme.error),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'需要登录', // TODO: Localize
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'请先登录以访问和管理 AI 配置。', // TODO: Localize
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// TODO: Implement navigation to login screen
|
||||
onClose(); // Close panel for now
|
||||
},
|
||||
child: const Text('前往登录'), // TODO: Localize
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
551
AINoval/lib/screens/editor/managers/editor_layout_manager.dart
Normal file
551
AINoval/lib/screens/editor/managers/editor_layout_manager.dart
Normal file
@@ -0,0 +1,551 @@
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:collection/collection.dart'; // For firstWhereOrNull
|
||||
|
||||
/// 编辑器布局管理器
|
||||
/// 负责管理编辑器的布局和尺寸
|
||||
class EditorLayoutManager extends ChangeNotifier {
|
||||
EditorLayoutManager() {
|
||||
_loadSavedDimensions();
|
||||
}
|
||||
|
||||
// 对象dispose状态跟踪
|
||||
bool _isDisposed = false;
|
||||
|
||||
// 侧边栏可见性状态
|
||||
bool isEditorSidebarVisible = true;
|
||||
bool isAIChatSidebarVisible = false;
|
||||
bool isSettingsPanelVisible = false;
|
||||
bool isNovelSettingsVisible = false;
|
||||
bool isAISummaryPanelVisible = false;
|
||||
bool isAISceneGenerationPanelVisible = false;
|
||||
bool isAIContinueWritingPanelVisible = false;
|
||||
bool isAISettingGenerationPanelVisible = false;
|
||||
bool isPromptViewVisible = false;
|
||||
|
||||
// 多面板显示时的顺序和位置
|
||||
final List<String> visiblePanels = [];
|
||||
static const String aiChatPanel = 'aiChatPanel';
|
||||
static const String aiSummaryPanel = 'aiSummaryPanel';
|
||||
static const String aiScenePanel = 'aiScenePanel';
|
||||
static const String aiContinueWritingPanel = 'aiContinueWritingPanel';
|
||||
static const String aiSettingGenerationPanel = 'aiSettingGenerationPanel';
|
||||
|
||||
// 侧边栏宽度
|
||||
double editorSidebarWidth = 400;
|
||||
double chatSidebarWidth = 380;
|
||||
|
||||
// 多面板模式下的单个面板宽度
|
||||
Map<String, double> panelWidths = {
|
||||
aiChatPanel: 600, // 聊天侧边栏默认最大宽度打开
|
||||
aiSummaryPanel: 350, // 其他侧边栏保持当前宽度
|
||||
aiScenePanel: 350,
|
||||
aiContinueWritingPanel: 350,
|
||||
aiSettingGenerationPanel: 350,
|
||||
};
|
||||
|
||||
// 侧边栏宽度限制
|
||||
static const double minEditorSidebarWidth = 220;
|
||||
static const double maxEditorSidebarWidth = 400;
|
||||
static const double minChatSidebarWidth = 280;
|
||||
static const double maxChatSidebarWidth = 500;
|
||||
static const double minPanelWidth = 280;
|
||||
static const double maxPanelWidth = 600; // 提升二分之一:400 * 1.5 = 600
|
||||
|
||||
// 持久化键
|
||||
static const String editorSidebarWidthPrefKey = 'editor_sidebar_width';
|
||||
static const String chatSidebarWidthPrefKey = 'chat_sidebar_width';
|
||||
static const String panelWidthsPrefKey = 'multi_panel_widths';
|
||||
static const String visiblePanelsPrefKey = 'visible_panels';
|
||||
static const String lastHiddenPanelsPrefKey = 'last_hidden_panels';
|
||||
|
||||
// 保存隐藏前的面板配置
|
||||
List<String> _lastHiddenPanelsConfig = [];
|
||||
|
||||
// 布局变化标志 - 用于标识当前变化是否为纯布局变化
|
||||
bool _isLayoutOnlyChange = false;
|
||||
|
||||
// 操作节流控制
|
||||
DateTime? _lastLayoutChangeTime;
|
||||
static const Duration _layoutChangeThrottle = Duration(milliseconds: 200);
|
||||
|
||||
// 获取是否为纯布局变化
|
||||
bool get isLayoutOnlyChange => _isLayoutOnlyChange;
|
||||
|
||||
// 重置布局变化标志
|
||||
void resetLayoutChangeFlag() {
|
||||
_isLayoutOnlyChange = false;
|
||||
}
|
||||
|
||||
// 🔧 优化:更严格的节流通知机制,避免在关键操作期间触发不必要的布局变化
|
||||
void _notifyLayoutChange() {
|
||||
if (_isDisposed) return; // 防止在dispose后调用
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
// 🔧 修复:更严格的节流控制,避免过于频繁的布局变化通知
|
||||
if (_lastLayoutChangeTime != null &&
|
||||
now.difference(_lastLayoutChangeTime!) < _layoutChangeThrottle) {
|
||||
// 在节流期间,仍然设置布局变化标志,但不触发通知
|
||||
_isLayoutOnlyChange = true;
|
||||
AppLogger.d('EditorLayoutManager', '节流: 跳过布局变化通知');
|
||||
return;
|
||||
}
|
||||
|
||||
_lastLayoutChangeTime = now;
|
||||
_isLayoutOnlyChange = true;
|
||||
|
||||
AppLogger.d('EditorLayoutManager', '触发布局变化通知');
|
||||
|
||||
// 立即通知监听器
|
||||
notifyListeners();
|
||||
|
||||
// 🔧 修复:延长标志重置时间,确保下游组件有足够时间处理布局变化
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (!_isDisposed) { // 检查对象是否仍然有效
|
||||
_isLayoutOnlyChange = false;
|
||||
AppLogger.d('EditorLayoutManager', '重置布局变化标志');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载保存的尺寸
|
||||
Future<void> _loadSavedDimensions() async {
|
||||
await _loadSavedEditorSidebarWidth();
|
||||
await _loadSavedChatSidebarWidth();
|
||||
await _loadSavedPanelWidths();
|
||||
await _loadSavedVisiblePanels();
|
||||
await _loadLastHiddenPanelsConfig();
|
||||
}
|
||||
|
||||
// 加载保存的编辑器侧边栏宽度
|
||||
Future<void> _loadSavedEditorSidebarWidth() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedWidth = prefs.getDouble(editorSidebarWidthPrefKey);
|
||||
if (savedWidth != null) {
|
||||
if (savedWidth >= minEditorSidebarWidth &&
|
||||
savedWidth <= maxEditorSidebarWidth) {
|
||||
editorSidebarWidth = savedWidth;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '加载编辑器侧边栏宽度失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存编辑器侧边栏宽度
|
||||
Future<void> saveEditorSidebarWidth() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(editorSidebarWidthPrefKey, editorSidebarWidth);
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '保存编辑器侧边栏宽度失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载保存的聊天侧边栏宽度
|
||||
Future<void> _loadSavedChatSidebarWidth() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedWidth = prefs.getDouble(chatSidebarWidthPrefKey);
|
||||
if (savedWidth != null) {
|
||||
if (savedWidth >= minChatSidebarWidth &&
|
||||
savedWidth <= maxChatSidebarWidth) {
|
||||
chatSidebarWidth = savedWidth;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '加载侧边栏宽度失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载保存的面板宽度
|
||||
Future<void> _loadSavedPanelWidths() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedWidthsString = prefs.getString(panelWidthsPrefKey);
|
||||
if (savedWidthsString != null) {
|
||||
final savedWidthsList = savedWidthsString.split(',');
|
||||
if (savedWidthsList.isNotEmpty) {
|
||||
// 聊天面板保持新的默认值(600),其他面板加载保存的值
|
||||
if (savedWidthsList.isNotEmpty && savedWidthsList[0].isNotEmpty) {
|
||||
final savedChatWidth = double.tryParse(savedWidthsList.elementAtOrNull(0) ?? '');
|
||||
if (savedChatWidth != null) {
|
||||
panelWidths[aiChatPanel] = savedChatWidth.clamp(minPanelWidth, maxPanelWidth);
|
||||
}
|
||||
}
|
||||
panelWidths[aiSummaryPanel] = double.tryParse(savedWidthsList.elementAtOrNull(1) ?? panelWidths[aiSummaryPanel].toString())!.clamp(minPanelWidth, maxPanelWidth);
|
||||
panelWidths[aiScenePanel] = double.tryParse(savedWidthsList.elementAtOrNull(2) ?? panelWidths[aiScenePanel].toString())!.clamp(minPanelWidth, maxPanelWidth);
|
||||
if (savedWidthsList.length > 3) {
|
||||
panelWidths[aiContinueWritingPanel] = double.tryParse(savedWidthsList.elementAtOrNull(3) ?? panelWidths[aiContinueWritingPanel].toString())!.clamp(minPanelWidth, maxPanelWidth);
|
||||
}
|
||||
if (savedWidthsList.length > 4) {
|
||||
panelWidths[aiSettingGenerationPanel] = double.tryParse(savedWidthsList.elementAtOrNull(4) ?? panelWidths[aiSettingGenerationPanel].toString())!.clamp(minPanelWidth, maxPanelWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '加载面板宽度失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载保存的可见面板
|
||||
Future<void> _loadSavedVisiblePanels() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedPanels = prefs.getStringList(visiblePanelsPrefKey);
|
||||
if (savedPanels != null) {
|
||||
visiblePanels.clear();
|
||||
visiblePanels.addAll(savedPanels);
|
||||
|
||||
// 更新各面板的可见性状态
|
||||
isAIChatSidebarVisible = visiblePanels.contains(aiChatPanel);
|
||||
isAISummaryPanelVisible = visiblePanels.contains(aiSummaryPanel);
|
||||
isAISceneGenerationPanelVisible = visiblePanels.contains(aiScenePanel);
|
||||
isAIContinueWritingPanelVisible = visiblePanels.contains(aiContinueWritingPanel);
|
||||
isAISettingGenerationPanelVisible = visiblePanels.contains(aiSettingGenerationPanel);
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '加载可见面板失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存聊天侧边栏宽度
|
||||
Future<void> saveChatSidebarWidth() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(chatSidebarWidthPrefKey, chatSidebarWidth);
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '保存侧边栏宽度失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存面板宽度
|
||||
Future<void> savePanelWidths() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final widthsString = [
|
||||
panelWidths[aiChatPanel],
|
||||
panelWidths[aiSummaryPanel],
|
||||
panelWidths[aiScenePanel],
|
||||
panelWidths[aiContinueWritingPanel],
|
||||
panelWidths[aiSettingGenerationPanel]
|
||||
].join(',');
|
||||
await prefs.setString(panelWidthsPrefKey, widthsString);
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '保存面板宽度失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存可见面板
|
||||
Future<void> saveVisiblePanels() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(visiblePanelsPrefKey, visiblePanels);
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '保存可见面板失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载隐藏前的面板配置
|
||||
Future<void> _loadLastHiddenPanelsConfig() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final savedConfig = prefs.getStringList(lastHiddenPanelsPrefKey);
|
||||
if (savedConfig != null) {
|
||||
_lastHiddenPanelsConfig = savedConfig;
|
||||
}
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '加载隐藏面板配置失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存隐藏前的面板配置
|
||||
Future<void> _saveLastHiddenPanelsConfig() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(lastHiddenPanelsPrefKey, _lastHiddenPanelsConfig);
|
||||
} catch (e) {
|
||||
AppLogger.e('EditorLayoutManager', '保存隐藏面板配置失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新编辑器侧边栏宽度
|
||||
void updateEditorSidebarWidth(double delta) {
|
||||
editorSidebarWidth = (editorSidebarWidth + delta).clamp(
|
||||
minEditorSidebarWidth,
|
||||
maxEditorSidebarWidth,
|
||||
);
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 更新聊天侧边栏宽度
|
||||
void updateChatSidebarWidth(double delta) {
|
||||
chatSidebarWidth = (chatSidebarWidth - delta).clamp(
|
||||
minChatSidebarWidth,
|
||||
maxChatSidebarWidth,
|
||||
);
|
||||
_notifyLayoutChange(); // 修复:添加missing的notifyListeners调用
|
||||
}
|
||||
|
||||
// 更新指定面板宽度
|
||||
void updatePanelWidth(String panelId, double delta) {
|
||||
if (panelWidths.containsKey(panelId)) {
|
||||
panelWidths[panelId] = (panelWidths[panelId]! - delta).clamp(
|
||||
minPanelWidth,
|
||||
maxPanelWidth,
|
||||
);
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
}
|
||||
|
||||
// 切换编辑器侧边栏可见性
|
||||
void toggleEditorSidebar() {
|
||||
isEditorSidebarVisible = !isEditorSidebarVisible;
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 抽屉模式切换:当宽度小于阈值时展开到最大,当宽度大于等于阈值时收起到抽屉阈值
|
||||
void toggleEditorSidebarCompactMode() {
|
||||
const double drawerThreshold = 260.0;
|
||||
if (editorSidebarWidth < drawerThreshold) {
|
||||
expandEditorSidebarToMax();
|
||||
} else {
|
||||
collapseEditorSidebarToDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
// 收起到抽屉(通过设置较小宽度触发精简抽屉UI)
|
||||
void collapseEditorSidebarToDrawer() {
|
||||
editorSidebarWidth = minEditorSidebarWidth; // e.g. 220,会触发 < 260 的精简抽屉
|
||||
_notifyLayoutChange();
|
||||
saveEditorSidebarWidth();
|
||||
}
|
||||
|
||||
// 展开到最大宽度
|
||||
void expandEditorSidebarToMax() {
|
||||
editorSidebarWidth = maxEditorSidebarWidth; // e.g. 400
|
||||
_notifyLayoutChange();
|
||||
saveEditorSidebarWidth();
|
||||
}
|
||||
|
||||
// 显示编辑器侧边栏(幂等)
|
||||
void showEditorSidebar() {
|
||||
if (!isEditorSidebarVisible) {
|
||||
isEditorSidebarVisible = true;
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏编辑器侧边栏(幂等)
|
||||
void hideEditorSidebar() {
|
||||
if (isEditorSidebarVisible) {
|
||||
isEditorSidebarVisible = false;
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
// 切换AI聊天侧边栏可见性
|
||||
void toggleAIChatSidebar() {
|
||||
// 在多面板模式下
|
||||
if (visiblePanels.contains(aiChatPanel)) {
|
||||
// 如果已经可见,则移除
|
||||
visiblePanels.remove(aiChatPanel);
|
||||
isAIChatSidebarVisible = false;
|
||||
} else {
|
||||
// 如果不可见,则添加
|
||||
visiblePanels.add(aiChatPanel);
|
||||
isAIChatSidebarVisible = true;
|
||||
}
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 切换AI场景生成面板可见性
|
||||
void toggleAISceneGenerationPanel() {
|
||||
// 在多面板模式下
|
||||
if (visiblePanels.contains(aiScenePanel)) {
|
||||
// 如果已经可见,则移除
|
||||
visiblePanels.remove(aiScenePanel);
|
||||
isAISceneGenerationPanelVisible = false;
|
||||
} else {
|
||||
// 如果不可见,则添加
|
||||
visiblePanels.add(aiScenePanel);
|
||||
isAISceneGenerationPanelVisible = true;
|
||||
}
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 切换AI摘要面板可见性
|
||||
void toggleAISummaryPanel() {
|
||||
// 在多面板模式下
|
||||
if (visiblePanels.contains(aiSummaryPanel)) {
|
||||
// 如果已经可见,则移除
|
||||
visiblePanels.remove(aiSummaryPanel);
|
||||
isAISummaryPanelVisible = false;
|
||||
} else {
|
||||
// 如果不可见,则添加
|
||||
visiblePanels.add(aiSummaryPanel);
|
||||
isAISummaryPanelVisible = true;
|
||||
}
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 新增:切换AI自动续写面板可见性
|
||||
void toggleAIContinueWritingPanel() {
|
||||
if (visiblePanels.contains(aiContinueWritingPanel)) {
|
||||
visiblePanels.remove(aiContinueWritingPanel);
|
||||
isAIContinueWritingPanelVisible = false;
|
||||
} else {
|
||||
visiblePanels.add(aiContinueWritingPanel);
|
||||
isAIContinueWritingPanelVisible = true;
|
||||
}
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 切换设置面板可见性
|
||||
void toggleSettingsPanel() {
|
||||
isSettingsPanelVisible = !isSettingsPanelVisible;
|
||||
if (isSettingsPanelVisible) {
|
||||
// 设置面板是全屏遮罩,不影响其他面板的显示
|
||||
}
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 切换小说设置视图可见性
|
||||
void toggleNovelSettings() {
|
||||
isNovelSettingsVisible = !isNovelSettingsVisible;
|
||||
if (isNovelSettingsVisible) {
|
||||
// 小说设置视图会替换主编辑区域,不影响侧边面板
|
||||
}
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 获取面板是否为最后一个
|
||||
bool isLastPanel(String panelId) {
|
||||
return visiblePanels.length == 1 && visiblePanels.contains(panelId);
|
||||
}
|
||||
|
||||
// 重新排序面板
|
||||
void reorderPanels(int oldIndex, int newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final item = visiblePanels.removeAt(oldIndex);
|
||||
visiblePanels.insert(newIndex, item);
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
void toggleAISettingGenerationPanel() {
|
||||
if (visiblePanels.contains(aiSettingGenerationPanel)) {
|
||||
visiblePanels.remove(aiSettingGenerationPanel);
|
||||
isAISettingGenerationPanelVisible = false;
|
||||
} else {
|
||||
visiblePanels.add(aiSettingGenerationPanel);
|
||||
isAISettingGenerationPanelVisible = true;
|
||||
}
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 切换提示词视图可见性
|
||||
void togglePromptView() {
|
||||
isPromptViewVisible = !isPromptViewVisible;
|
||||
if (isPromptViewVisible) {
|
||||
// 提示词视图是全屏替换,不影响其他面板的显示
|
||||
}
|
||||
_notifyLayoutChange(); // 使用布局专用的通知方法
|
||||
}
|
||||
|
||||
// 🚀 新增:沉浸模式状态管理
|
||||
bool isImmersiveModeEnabled = false;
|
||||
|
||||
// 🚀 新增:切换沉浸模式
|
||||
void toggleImmersiveMode() {
|
||||
isImmersiveModeEnabled = !isImmersiveModeEnabled;
|
||||
AppLogger.i('EditorLayoutManager', '切换沉浸模式: $isImmersiveModeEnabled');
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
|
||||
// 🚀 新增:启用沉浸模式
|
||||
void enableImmersiveMode() {
|
||||
if (!isImmersiveModeEnabled) {
|
||||
isImmersiveModeEnabled = true;
|
||||
AppLogger.i('EditorLayoutManager', '启用沉浸模式');
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 新增:禁用沉浸模式
|
||||
void disableImmersiveMode() {
|
||||
if (isImmersiveModeEnabled) {
|
||||
isImmersiveModeEnabled = false;
|
||||
AppLogger.i('EditorLayoutManager', '禁用沉浸模式');
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// 隐藏所有AI面板
|
||||
void hideAllAIPanels() {
|
||||
if (visiblePanels.isNotEmpty) {
|
||||
// 保存当前配置
|
||||
_lastHiddenPanelsConfig = List<String>.from(visiblePanels);
|
||||
_saveLastHiddenPanelsConfig();
|
||||
|
||||
// 隐藏所有面板
|
||||
visiblePanels.clear();
|
||||
isAIChatSidebarVisible = false;
|
||||
isAISummaryPanelVisible = false;
|
||||
isAISceneGenerationPanelVisible = false;
|
||||
isAIContinueWritingPanelVisible = false;
|
||||
isAISettingGenerationPanelVisible = false;
|
||||
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复隐藏前的AI面板配置
|
||||
void restoreHiddenAIPanels() {
|
||||
if (_lastHiddenPanelsConfig.isNotEmpty) {
|
||||
// 恢复面板配置
|
||||
visiblePanels.clear();
|
||||
visiblePanels.addAll(_lastHiddenPanelsConfig);
|
||||
|
||||
// 更新各面板的可见性状态
|
||||
isAIChatSidebarVisible = visiblePanels.contains(aiChatPanel);
|
||||
isAISummaryPanelVisible = visiblePanels.contains(aiSummaryPanel);
|
||||
isAISceneGenerationPanelVisible = visiblePanels.contains(aiScenePanel);
|
||||
isAIContinueWritingPanelVisible = visiblePanels.contains(aiContinueWritingPanel);
|
||||
isAISettingGenerationPanelVisible = visiblePanels.contains(aiSettingGenerationPanel);
|
||||
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange();
|
||||
} else {
|
||||
// 如果没有保存的配置,显示默认的AI聊天面板
|
||||
toggleAIChatSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示AI摘要面板
|
||||
void showAISummaryPanel() {
|
||||
if (!visiblePanels.contains(aiSummaryPanel)) {
|
||||
visiblePanels.add(aiSummaryPanel);
|
||||
isAISummaryPanelVisible = true;
|
||||
saveVisiblePanels();
|
||||
_notifyLayoutChange();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
319
AINoval/lib/screens/editor/managers/editor_state_manager.dart
Normal file
319
AINoval/lib/screens/editor/managers/editor_state_manager.dart
Normal file
@@ -0,0 +1,319 @@
|
||||
import 'package:ainoval/blocs/editor/editor_bloc.dart' as editor_bloc;
|
||||
import 'package:ainoval/models/novel_structure.dart' as novel_models;
|
||||
import 'package:ainoval/utils/logger.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 编辑器状态管理器
|
||||
/// 负责管理编辑器的状态,如字数统计、控制器检查等
|
||||
class EditorStateManager {
|
||||
EditorStateManager();
|
||||
|
||||
// 控制器检查节流相关变量
|
||||
DateTime? _lastControllerCheckTime;
|
||||
static const Duration _controllerCheckInterval = Duration(milliseconds: 500);
|
||||
static const Duration _controllerLongCheckInterval = Duration(seconds: 5);
|
||||
editor_bloc.EditorLoaded? _lastEditorState;
|
||||
|
||||
// 字数统计缓存
|
||||
int _cachedWordCount = 0;
|
||||
String? _wordCountCacheKey;
|
||||
final Map<String, int> _memoryWordCountCache = {};
|
||||
|
||||
// 🔧 新增:模型验证状态跟踪,防止模型操作影响编辑器状态
|
||||
bool _isModelOperationInProgress = false;
|
||||
DateTime? _lastModelOperationTime;
|
||||
static const Duration _modelOperationCooldown = Duration(seconds: 5);
|
||||
|
||||
// 🔧 新增:设置模型操作状态
|
||||
void setModelOperationInProgress(bool inProgress) {
|
||||
_isModelOperationInProgress = inProgress;
|
||||
if (inProgress) {
|
||||
_lastModelOperationTime = DateTime.now();
|
||||
AppLogger.i('EditorStateManager', '模型操作开始,暂停控制器检查');
|
||||
} else {
|
||||
AppLogger.i('EditorStateManager', '模型操作结束');
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 新增:检查是否在模型操作冷却期
|
||||
bool get _isInModelOperationCooldown {
|
||||
if (_lastModelOperationTime == null) return false;
|
||||
final now = DateTime.now();
|
||||
final inCooldown = now.difference(_lastModelOperationTime!) < _modelOperationCooldown;
|
||||
if (inCooldown) {
|
||||
AppLogger.d('EditorStateManager', '模型操作冷却期中,跳过控制器检查');
|
||||
}
|
||||
return inCooldown;
|
||||
}
|
||||
|
||||
// 清除内存缓存
|
||||
void clearMemoryCache() {
|
||||
_memoryWordCountCache.clear();
|
||||
}
|
||||
|
||||
// 计算总字数
|
||||
int calculateTotalWordCount(novel_models.Novel novel) {
|
||||
// 生成缓存键:使用更新时间和场景总数作为缓存键
|
||||
final totalSceneCount = novel.acts.fold(0, (sum, act) =>
|
||||
sum + act.chapters.fold(0, (sum, chapter) =>
|
||||
sum + chapter.scenes.length));
|
||||
|
||||
final updatedAtMs = novel.updatedAt.millisecondsSinceEpoch ?? 0;
|
||||
final cacheKey = '${novel.id}_${updatedAtMs}_$totalSceneCount';
|
||||
|
||||
// 首先检查内存缓存,这是最快的检查方式
|
||||
if (_memoryWordCountCache.containsKey(cacheKey)) {
|
||||
// 完全跳过日志记录以提高性能
|
||||
return _memoryWordCountCache[cacheKey]!;
|
||||
}
|
||||
|
||||
// 如果持久化缓存有效,直接返回缓存的字数
|
||||
if (cacheKey == _wordCountCacheKey && _cachedWordCount > 0) {
|
||||
// 同时更新内存缓存
|
||||
_memoryWordCountCache[cacheKey] = _cachedWordCount;
|
||||
return _cachedWordCount;
|
||||
}
|
||||
|
||||
// 检查是否在滚动过程中 - 如果在滚动,使用旧缓存或返回0而不是计算
|
||||
final now = DateTime.now();
|
||||
if (_lastScrollHandleTime != null &&
|
||||
now.difference(_lastScrollHandleTime!) < const Duration(seconds: 2)) {
|
||||
// 在滚动过程中,如果有缓存直接用,没有就返回0避免计算
|
||||
if (_cachedWordCount > 0) {
|
||||
AppLogger.d('EditorStateManager', '滚动中使用缓存字数: $_cachedWordCount');
|
||||
// 同时更新内存缓存
|
||||
_memoryWordCountCache[cacheKey] = _cachedWordCount;
|
||||
return _cachedWordCount;
|
||||
} else {
|
||||
AppLogger.d('EditorStateManager', '滚动中跳过字数计算');
|
||||
return 0; // 返回0避免计算
|
||||
}
|
||||
}
|
||||
|
||||
// 正常情况下,记录字数计算原因
|
||||
AppLogger.i('EditorStateManager', '字数统计缓存无效,重新计算。新缓存键: $cacheKey,旧缓存键: ${_wordCountCacheKey ?? "无"}');
|
||||
|
||||
// 计算总字数(不再重复计算每个场景的字数)
|
||||
int totalWordCount = 0;
|
||||
for (final act in novel.acts) {
|
||||
for (final chapter in act.chapters) {
|
||||
for (final scene in chapter.scenes) {
|
||||
// 直接使用存储的字数,不重新计算
|
||||
totalWordCount += scene.wordCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新缓存,并减少日志输出
|
||||
_wordCountCacheKey = cacheKey;
|
||||
_cachedWordCount = totalWordCount;
|
||||
|
||||
// 同时更新内存缓存
|
||||
_memoryWordCountCache[cacheKey] = totalWordCount;
|
||||
|
||||
AppLogger.i('EditorStateManager', '小说总字数计算结果: $totalWordCount (Acts: ${novel.acts.length}, 更新缓存键: $cacheKey)');
|
||||
return totalWordCount;
|
||||
}
|
||||
|
||||
// 滚动处理节流
|
||||
DateTime? _lastScrollHandleTime;
|
||||
|
||||
// 检查是否应该重建Quill控制器
|
||||
bool shouldCheckControllers(editor_bloc.EditorLoaded state, {bool isLayoutOnlyChange = false}) {
|
||||
if (_isModelOperationInProgress || _isInModelOperationCooldown) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果是纯布局变化,跳过控制器检查
|
||||
if (isLayoutOnlyChange) {
|
||||
if (kDebugMode) {
|
||||
AppLogger.d('EditorStateManager', '跳过控制器检查 - 原因: 纯布局变化');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.lastUpdateSilent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果状态对象引用变化,表示小说数据结构可能发生变化,需要检查
|
||||
final bool stateChanged = _lastEditorState != state;
|
||||
final now = DateTime.now();
|
||||
|
||||
// 检查是否刚完成加载且内容有变化 (最重要的条件)
|
||||
bool justFinishedLoadingWithChanges = false;
|
||||
bool contentChanged = false; // Calculate contentChanged regardless of other checks
|
||||
|
||||
if (stateChanged && _lastEditorState != null) {
|
||||
// 检查小说结构是否有实质变化,主要比较acts和scenes的数量
|
||||
final oldNovel = _lastEditorState!.novel;
|
||||
final newNovel = state.novel;
|
||||
|
||||
// 🔧 修复:更严格的内容变化检查,避免将非内容变化误认为内容变化
|
||||
// 只有在小说结构本身发生变化时才认为是内容变化
|
||||
|
||||
// 首先检查小说基本信息是否变化(排除时间戳)
|
||||
if (oldNovel.id != newNovel.id ||
|
||||
oldNovel.title != newNovel.title) {
|
||||
contentChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到小说基本信息变化');
|
||||
}
|
||||
|
||||
// 检查act数量是否变化
|
||||
else if (oldNovel.acts.length != newNovel.acts.length) {
|
||||
contentChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到Act数量变化: ${oldNovel.acts.length} -> ${newNovel.acts.length}');
|
||||
}
|
||||
else {
|
||||
// 检查章节和场景数量是否变化
|
||||
bool structureChanged = false;
|
||||
|
||||
for (int i = 0; i < oldNovel.acts.length && i < newNovel.acts.length; i++) {
|
||||
final oldAct = oldNovel.acts[i];
|
||||
final newAct = newNovel.acts[i];
|
||||
|
||||
// 检查Act基本信息
|
||||
if (oldAct.id != newAct.id || oldAct.title != newAct.title) {
|
||||
structureChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到Act[$i]基本信息变化');
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查章节数量
|
||||
if (oldAct.chapters.length != newAct.chapters.length) {
|
||||
structureChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到Act[$i]章节数量变化: ${oldAct.chapters.length} -> ${newAct.chapters.length}');
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查每个章节的场景数量
|
||||
for (int j = 0; j < oldAct.chapters.length && j < newAct.chapters.length; j++) {
|
||||
final oldChapter = oldAct.chapters[j];
|
||||
final newChapter = newAct.chapters[j];
|
||||
|
||||
// 检查Chapter基本信息
|
||||
if (oldChapter.id != newChapter.id || oldChapter.title != newChapter.title) {
|
||||
structureChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到Chapter[$i][$j]基本信息变化');
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查场景数量
|
||||
if (oldChapter.scenes.length != newChapter.scenes.length) {
|
||||
structureChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到Chapter[$i][$j]场景数量变化: ${oldChapter.scenes.length} -> ${newChapter.scenes.length}');
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查场景ID是否变化(新增/删除场景)
|
||||
final oldSceneIds = oldChapter.scenes.map((s) => s.id).toSet();
|
||||
final newSceneIds = newChapter.scenes.map((s) => s.id).toSet();
|
||||
if (oldSceneIds.length != newSceneIds.length ||
|
||||
!oldSceneIds.containsAll(newSceneIds) ||
|
||||
!newSceneIds.containsAll(oldSceneIds)) {
|
||||
structureChanged = true;
|
||||
AppLogger.i('EditorStateManager', '检测到Chapter[$i][$j]场景ID变化');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (structureChanged) break;
|
||||
}
|
||||
|
||||
contentChanged = structureChanged;
|
||||
}
|
||||
|
||||
// *** Check if loading just finished and content actually changed ***
|
||||
if (_lastEditorState!.isLoading && !state.isLoading && contentChanged) {
|
||||
justFinishedLoadingWithChanges = true;
|
||||
// 仅在调试模式下记录日志
|
||||
if (kDebugMode) {
|
||||
AppLogger.i('EditorStateManager', '检测到加载完成且内容有变化,强制检查控制器。');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// *** Bypass throttle if loading just finished with changes ***
|
||||
if (justFinishedLoadingWithChanges) {
|
||||
_lastControllerCheckTime = now;
|
||||
_lastEditorState = state; // Update state reference
|
||||
// 仅在调试模式下记录日志
|
||||
if (kDebugMode) {
|
||||
AppLogger.i('EditorStateManager', '触发控制器检查 - 原因: 加载完成');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 🔧 修复:增加节流时间到15秒,减少不必要的控制器检查
|
||||
// 极端节流:如果距离上次检查时间不足15秒,且不是刚加载完成,绝对不检查
|
||||
if (_lastControllerCheckTime != null &&
|
||||
now.difference(_lastControllerCheckTime!) < const Duration(seconds: 15)) {
|
||||
// 记录日志:禁止频繁检查 (仅在状态变化且调试模式下记录,避免日志刷屏)
|
||||
if (stateChanged && kDebugMode) {
|
||||
AppLogger.d('EditorStateManager', '节流: 禁止15秒内重复检查控制器');
|
||||
}
|
||||
// 更新状态引用,即使被节流也要更新,以便下次比较
|
||||
_lastEditorState = state;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查活动元素是否变化
|
||||
bool activeElementsChanged = false;
|
||||
if (stateChanged && _lastEditorState != null) {
|
||||
activeElementsChanged =
|
||||
_lastEditorState!.activeActId != state.activeActId ||
|
||||
_lastEditorState!.activeChapterId != state.activeChapterId ||
|
||||
_lastEditorState!.activeSceneId != state.activeSceneId;
|
||||
}
|
||||
|
||||
// 🔧 修复:只有在以下严格条件下才重建控制器
|
||||
// 1. 首次加载(_lastControllerCheckTime为null)
|
||||
// 2. 确实的内容结构变化(添加/删除场景或章节)
|
||||
// 3. 活动元素变化
|
||||
// 4. 长时间间隔超时 (15秒)
|
||||
final bool timeIntervalExceeded = _lastControllerCheckTime == null ||
|
||||
now.difference(_lastControllerCheckTime!) > const Duration(seconds: 15);
|
||||
|
||||
final bool needsCheck = _lastControllerCheckTime == null ||
|
||||
contentChanged ||
|
||||
activeElementsChanged ||
|
||||
timeIntervalExceeded;
|
||||
|
||||
// 更新状态引用,用于下次比较
|
||||
_lastEditorState = state;
|
||||
|
||||
// 如果需要检查,更新最后检查时间
|
||||
if (needsCheck) {
|
||||
_lastControllerCheckTime = now;
|
||||
|
||||
// 仅在调试模式下记录日志
|
||||
if (kDebugMode) {
|
||||
String reason;
|
||||
if (contentChanged) {
|
||||
reason = '内容结构变化';
|
||||
} else if (activeElementsChanged) {
|
||||
reason = '活动元素变化';
|
||||
} else if (timeIntervalExceeded) {
|
||||
reason = '时间间隔超过(15秒)';
|
||||
} else {
|
||||
reason = '首次加载';
|
||||
}
|
||||
|
||||
AppLogger.i('EditorStateManager', '触发控制器检查 - 原因: $reason');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 内容更新通知器
|
||||
final ValueNotifier<String> contentUpdateNotifier = ValueNotifier<String>('');
|
||||
|
||||
// 通知内容更新
|
||||
void notifyContentUpdate(String reason) {
|
||||
AppLogger.i('EditorStateManager', '通知内容更新: $reason');
|
||||
contentUpdateNotifier.value = '${DateTime.now().millisecondsSinceEpoch}_$reason';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user