马良AI写作初始化仓库

This commit is contained in:
邓滨杰
2025-09-10 00:07:52 +08:00
parent 3c06bb1a03
commit 39c0f8840f
1309 changed files with 318528 additions and 0 deletions

View 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
)
],
),
),
);
}
}

View 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();
}
}

View 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';
}
}