import 'package:ainoval/blocs/editor/editor_bloc.dart' as editor_bloc; import 'package:ainoval/models/editor_settings.dart'; import 'package:ainoval/screens/editor/components/draggable_divider.dart'; import 'package:ainoval/screens/editor/components/editor_app_bar.dart'; import 'package:ainoval/screens/editor/components/editor_main_area.dart'; import 'package:ainoval/screens/editor/components/editor_sidebar.dart'; import 'package:ainoval/screens/editor/components/fullscreen_loading_overlay.dart'; import 'package:ainoval/screens/editor/components/multi_ai_panel_view.dart'; import 'package:ainoval/screens/editor/components/plan_view.dart'; import 'package:ainoval/screens/editor/controllers/editor_screen_controller.dart'; import 'package:ainoval/screens/editor/managers/editor_dialog_manager.dart'; import 'package:ainoval/screens/editor/managers/editor_layout_manager.dart'; import 'package:ainoval/screens/editor/managers/editor_state_manager.dart'; import 'package:ainoval/screens/editor/widgets/novel_settings_view.dart'; import 'package:ainoval/screens/next_outline/next_outline_view.dart'; import 'package:ainoval/screens/settings/settings_panel.dart'; import 'package:ainoval/services/api_service/repositories/editor_repository.dart'; import 'package:ainoval/services/api_service/repositories/impl/aliyun_oss_storage_repository.dart'; import 'package:ainoval/services/api_service/repositories/impl/user_ai_model_config_repository_impl.dart'; import 'package:ainoval/services/api_service/repositories/prompt_repository.dart'; import 'package:ainoval/services/api_service/repositories/storage_repository.dart'; import 'package:ainoval/screens/unified_management/unified_management_screen.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:ainoval/widgets/common/top_toast.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:ainoval/utils/web_theme.dart'; /// 编辑器布局组件 /// 负责组织编辑器的整体布局 class EditorLayout extends StatelessWidget { const EditorLayout({ super.key, required this.controller, required this.layoutManager, required this.stateManager, this.onAutoContinueWritingPressed, }); final EditorScreenController controller; final EditorLayoutManager layoutManager; final EditorStateManager stateManager; final VoidCallback? onAutoContinueWritingPressed; @override Widget build(BuildContext context) { // 清除内存缓存,确保每次build周期都使用新的内存缓存 stateManager.clearMemoryCache(); // 监听 EditorScreenController 的状态变化,特别是 isFullscreenLoading return ChangeNotifierProvider.value( value: controller, child: Consumer( builder: (context, editorController, _) { // 主要布局,始终在Stack中 Widget mainContent; if (editorController.isFullscreenLoading) { // 如果正在全屏加载,主内容可以是空的,或者是一个基础占位符 // 因为FullscreenLoadingOverlay会覆盖它 mainContent = const SizedBox.shrink(); } else { // 正常的主布局 mainContent = ValueListenableBuilder( valueListenable: stateManager.contentUpdateNotifier, builder: (context, updateValue, child) { return BlocBuilder( bloc: editorController.editorBloc, buildWhen: (previous, current) { if (current is editor_bloc.EditorLoaded) { return current.lastUpdateSilent == false; } return true; }, builder: (context, state) { if (state is editor_bloc.EditorLoading) { return const Center(child: CircularProgressIndicator()); } else if (state is editor_bloc.EditorLoaded) { if (stateManager.shouldCheckControllers(state)) { editorController.ensureControllersForNovel(state.novel); } return _buildMainLayout(context, state, editorController, stateManager); } else if (state is editor_bloc.EditorError) { return Center(child: Text('错误: ${state.message}')); } else { return const Center(child: Text('未知状态')); } }, ); } ); } // 使用Stack来容纳主内容和可能的覆盖层,并包装性能监控面板 Widget stackContent = Stack( children: [ mainContent, if (editorController.isFullscreenLoading) FullscreenLoadingOverlay( loadingMessage: editorController.loadingMessage, showProgressIndicator: true, progress: editorController.loadingProgress >= 0 ? editorController.loadingProgress : -1, ), ], ); return stackContent; }, ), ); } // 构建主布局 Widget _buildMainLayout(BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController, EditorStateManager stateManager) { final screenWidth = MediaQuery.of(context).size.width; final bool isNarrow = screenWidth < 1280; final bool isVeryNarrow = screenWidth < 900; return Stack( children: [ // 🚀 修复:给主布局添加背景色容器 Container( color: Theme.of(context).scaffoldBackgroundColor, child: Row( children: [ // 左侧导航 - 监听布局管理器以响应宽度变化(保留抽屉逻辑,移除完全隐藏) Consumer( builder: (context, layoutState, child) { // 当宽度过小时,切换为“简要抽屉模式”:显示底部功能区的精简版,仅保留关键按钮和展开按钮 return LayoutBuilder( builder: (context, constraints) { final double effectiveSidebarWidth = layoutState.editorSidebarWidth.clamp( EditorLayoutManager.minEditorSidebarWidth, isVeryNarrow ? 260.0 : (isNarrow ? 300.0 : EditorLayoutManager.maxEditorSidebarWidth), ); final bool useCompactDrawer = effectiveSidebarWidth < 260 || isVeryNarrow; if (useCompactDrawer) { // 精简抽屉:固定窄栏,展示底部功能区简版 + 展开按钮 return Row( children: [ SizedBox( width: 64, child: _CompactSidebarDrawer( onExpand: () => layoutState.expandEditorSidebarToMax(), onOpenSettings: () => layoutState.toggleNovelSettings(), onOpenAIChat: () => layoutState.toggleAIChatSidebar(), ), ), // 在精简模式下保留分隔线,允许用户拖动扩大回正常模式 DraggableDivider( onDragUpdate: (delta) { layoutState.updateEditorSidebarWidth(delta.delta.dx); }, onDragEnd: (_) { layoutState.saveEditorSidebarWidth(); }, ), ], ); } // 正常模式 return Row( children: [ SizedBox( width: effectiveSidebarWidth, child: EditorSidebar( novel: editorController.novel, tabController: editorController.tabController, onOpenAIChat: () { context.read().toggleAIChatSidebar(); }, onOpenSettings: () { context.read().toggleNovelSettings(); }, onToggleSidebar: () { context.read().toggleEditorSidebarCompactMode(); }, onAdjustWidth: () => _showEditorSidebarWidthDialog(context), ), ), DraggableDivider( onDragUpdate: (delta) { context.read().updateEditorSidebarWidth(delta.delta.dx); }, onDragEnd: (_) { context.read().saveEditorSidebarWidth(); }, ), ], ); }, ); }, ), // 主编辑区域 - 完全不监听EditorLayoutManager的变化 Expanded( child: Column( children: [ // 编辑器顶部工具栏和操作栏 BlocBuilder( buildWhen: (prev, curr) => curr is editor_bloc.EditorLoaded, builder: (context, blocState) { final editorState = blocState as editor_bloc.EditorLoaded; return Consumer( builder: (context, layoutState, child) { if (layoutState.isNovelSettingsVisible) { return const SizedBox(height: kToolbarHeight); } return EditorAppBar( novelTitle: editorController.novel.title, wordCount: stateManager.calculateTotalWordCount(editorState.novel), isSaving: editorState.isSaving, isDirty: editorState.isDirty, lastSaveTime: editorState.lastSaveTime, onBackPressed: () => Navigator.pop(context), onChatPressed: layoutState.toggleAIChatSidebar, isChatActive: layoutState.isAIChatSidebarVisible, onAiConfigPressed: layoutState.toggleSettingsPanel, isSettingsActive: layoutState.isSettingsPanelVisible, onPlanPressed: editorController.togglePlanView, isPlanActive: editorController.isPlanViewActive, isWritingActive: !editorController.isPlanViewActive && !editorController.isNextOutlineViewActive && !editorController.isPromptViewActive, onWritePressed: (editorController.isPlanViewActive || editorController.isNextOutlineViewActive || editorController.isPromptViewActive) ? () { if (editorController.isPlanViewActive) { editorController.togglePlanView(); } else if (editorController.isNextOutlineViewActive) { editorController.toggleNextOutlineView(); } else if (editorController.isPromptViewActive) { editorController.togglePromptView(); } } : null, onNextOutlinePressed: editorController.toggleNextOutlineView, onAIGenerationPressed: layoutState.toggleAISceneGenerationPanel, onAISummaryPressed: layoutState.toggleAISummaryPanel, onAutoContinueWritingPressed: layoutState.toggleAIContinueWritingPanel, onAISettingGenerationPressed: layoutState.toggleAISettingGenerationPanel, isAIGenerationActive: layoutState.isAISceneGenerationPanelVisible || layoutState.isAISummaryPanelVisible || layoutState.isAIContinueWritingPanelVisible, isAISummaryActive: layoutState.isAISummaryPanelVisible, isAIContinueWritingActive: layoutState.isAIContinueWritingPanelVisible, isAISettingGenerationActive: layoutState.isAISettingGenerationPanelVisible, isNextOutlineActive: editorController.isNextOutlineViewActive, // 🚀 新增:传递编辑器BLoC实例给沉浸模式 editorBloc: editorController.editorBloc, ); }, ); }, ), // 主编辑区域内容 - 移除右侧AI面板,只保留主编辑器内容 Expanded( child: _buildMainEditorContentOnly(context, editorBlocState, editorController), ), ], ), ), // 右侧AI面板区域 - 大屏时并排显示,小屏改为覆盖式(在覆盖层中渲染) if (!isNarrow) _buildRightAIPanelArea(context, editorBlocState, editorController), ], ), ), // 覆盖层组件 - 使用Consumer监听必要的状态 // 移除“完全隐藏左侧栏”的开关按钮覆盖层,仅保留其他覆盖层 ..._buildOverlayWidgets(context, editorBlocState, editorController, stateManager) .where((w) { // 过滤掉依赖 isEditorSidebarVisible 的侧边栏切换按钮 // 该按钮在 _buildOverlayWidgets 中是第一个元素(Selector),这里不再添加 // 实现方式:在 _buildOverlayWidgets 内部保留原实现,这里不使用第一个返回项 return true; }), // 小屏右侧AI面板覆盖式展示 _buildRightPanelOverlayIfNeeded(context, editorBlocState, editorController, isNarrow: isNarrow), ], ); } // 构建主编辑器内容(不包含右侧AI面板) Widget _buildMainEditorContentOnly(BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController) { // 主编辑器内容区域 - 监听小说设置状态变化 return Selector( selector: (context, layoutManager) => layoutManager.isNovelSettingsVisible, builder: (context, isNovelSettingsVisible, child) { if (isNovelSettingsVisible) { return MultiRepositoryProvider( providers: [ RepositoryProvider( create: (context) => editorController.editorRepository, ), RepositoryProvider( create: (context) => AliyunOssStorageRepository(editorController.apiClient), ), ], child: NovelSettingsView( novel: editorController.novel, onSettingsClose: () { context.read().toggleNovelSettings(); }, ), ); } // 🚀 关键修复:使用Stack布局,保持EditorMainArea不被销毁 return Stack( children: [ // EditorMainArea始终存在,只是可能被隐藏 Visibility( visible: !editorController.isPlanViewActive && !editorController.isNextOutlineViewActive && !editorController.isPromptViewActive, maintainState: true, // 保持状态,避免重建 child: EditorMainArea( key: editorController.editorMainAreaKey, novel: editorBlocState.novel, editorBloc: editorController.editorBloc, sceneControllers: editorController.sceneControllers, sceneSummaryControllers: editorController.sceneSummaryControllers, activeActId: editorBlocState.activeActId, activeChapterId: editorBlocState.activeChapterId, activeSceneId: editorBlocState.activeSceneId, scrollController: editorController.scrollController, sceneKeys: editorController.sceneKeys, // 🚀 新增:传递编辑器设置给EditorMainArea editorSettings: EditorSettings.fromMap(editorBlocState.settings), ), ), // Plan视图覆盖在上层 if (editorController.isPlanViewActive) PlanView( novelId: editorController.novel.id, editorBloc: editorController.editorBloc, onSwitchToWrite: editorController.togglePlanView, ), // NextOutline视图覆盖在上层 if (editorController.isNextOutlineViewActive) NextOutlineView( novelId: editorController.novel.id, novelTitle: editorController.novel.title, onSwitchToWrite: editorController.toggleNextOutlineView, ), // 统一管理视图覆盖在上层 if (editorController.isPromptViewActive) const UnifiedManagementScreen(), ], ); }, ); } // 构建右侧AI面板区域 - 完整占据右边,从顶部到底部 Widget _buildRightAIPanelArea(BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController) { return Consumer( builder: (context, layoutManager, child) { final hasVisibleAIPanels = layoutManager.visiblePanels.isNotEmpty; if (!hasVisibleAIPanels) { return const SizedBox.shrink(); } return Row( children: [ // 面板分隔线 DraggableDivider( onDragUpdate: (delta) { if (layoutManager.visiblePanels.isNotEmpty) { final firstPanelId = layoutManager.visiblePanels.first; layoutManager.updatePanelWidth(firstPanelId, delta.delta.dx); } }, onDragEnd: (_) { layoutManager.savePanelWidths(); }, ), // AI面板组件 - 完整高度 RepositoryProvider( create: (context) => editorController.promptRepository, child: MultiAIPanelView( novelId: editorController.novel.id, chapterId: editorBlocState.activeChapterId, layoutManager: layoutManager, userId: editorController.currentUserId, userAiModelConfigRepository: UserAIModelConfigRepositoryImpl(apiClient: editorController.apiClient), editorRepository: editorController.editorRepository, novelAIRepository: editorController.novelAIRepository, onContinueWritingSubmit: (parameters) { AppLogger.i('EditorLayout', 'Continue Writing Submitted: $parameters'); TopToast.success(context, '自动续写任务已提交: $parameters'); }, ), ), ], ); }, ); } // 小屏时以覆盖层形式展示右侧AI面板 Widget _buildRightPanelOverlayIfNeeded( BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController, { required bool isNarrow, }) { if (!isNarrow) return const SizedBox.shrink(); final screenWidth = MediaQuery.of(context).size.width; return Consumer( builder: (context, layoutManager, child) { final hasVisibleAIPanels = layoutManager.visiblePanels.isNotEmpty; if (!hasVisibleAIPanels) return const SizedBox.shrink(); // 小屏覆盖式面板宽度:不超过屏宽的35%,并在全局最小/最大约束之间 final double maxRightPanelWidth = ( screenWidth * 0.35 ).clamp( EditorLayoutManager.minPanelWidth, EditorLayoutManager.maxPanelWidth, ); return Positioned.fill( child: Stack( children: [ // 半透明遮罩,点击关闭右侧所有AI面板 GestureDetector( onTap: () => layoutManager.hideAllAIPanels(), child: Container( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4), ), ), // 右侧贴边的覆盖面板 Align( alignment: Alignment.centerRight, child: Container( width: maxRightPanelWidth, height: double.infinity, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, boxShadow: [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.2), blurRadius: 12, offset: const Offset(0, 2), ), ], ), child: RepositoryProvider( create: (context) => editorController.promptRepository, child: MultiAIPanelView( novelId: editorController.novel.id, chapterId: editorBlocState.activeChapterId, layoutManager: layoutManager, userId: editorController.currentUserId, userAiModelConfigRepository: UserAIModelConfigRepositoryImpl(apiClient: editorController.apiClient), editorRepository: editorController.editorRepository, novelAIRepository: editorController.novelAIRepository, onContinueWritingSubmit: (parameters) { AppLogger.i('EditorLayout', 'Continue Writing Submitted: $parameters'); TopToast.success(context, '自动续写任务已提交: $parameters'); }, ), ), ), ), ], ), ); }, ); } // 构建覆盖层组件 List _buildOverlayWidgets(BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController, EditorStateManager stateManager) { return [ // 移除:不再提供“完全隐藏侧边栏”的开关按钮,保留其他覆盖层 // 设置面板 Selector( selector: (context, layoutManager) => layoutManager.isSettingsPanelVisible, builder: (context, isVisible, child) { if (!isVisible) return const SizedBox.shrink(); return Positioned.fill( child: GestureDetector( onTap: () => context.read().toggleSettingsPanel(), child: Container( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), child: Center( child: GestureDetector( onTap: () {}, child: editorController.currentUserId == null ? EditorDialogManager.buildLoginRequiredPanel( context, () => context.read().toggleSettingsPanel(), ) : SettingsPanel( stateManager: stateManager, userId: editorController.currentUserId!, onClose: () => context.read().toggleSettingsPanel(), editorSettings: EditorSettings.fromMap(editorBlocState.settings), onEditorSettingsChanged: (settings) { context.read().add( editor_bloc.UpdateEditorSettings(settings: settings.toMap())); }, initialCategoryIndex: SettingsPanel.accountManagementCategoryIndex, ), ), ), ), ), ); }, ), // 保存中浮动按钮 if (editorBlocState.isSaving) Positioned( right: 16, bottom: 16, child: FloatingActionButton( heroTag: 'saving', onPressed: null, backgroundColor: Theme.of(context).colorScheme.outlineVariant.withOpacity(0.6), tooltip: '正在保存...', child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(WebTheme.isDarkMode(context) ? WebTheme.darkGrey50 : WebTheme.white), ), ), ), ), // 加载动画覆盖层 (用于非全屏的 "加载更多") if ((editorBlocState.isLoading || editorController.isLoadingMore) && !editorController.isFullscreenLoading) _buildLoadingOverlay(context, editorController), ]; } // 构建加载动画覆盖层 Widget _buildEndOfContentIndicator(BuildContext context, String message) { return Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Text( message, style: TextStyle( color: WebTheme.getSecondaryTextColor(context), fontSize: 14, fontWeight: FontWeight.w500, ), ), ); } Widget _buildLoadingOverlay(BuildContext context, EditorScreenController editorController) { return Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.only(bottom: 32.0), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ WebTheme.getSurfaceColor(context).withAlpha(0), WebTheme.getSurfaceColor(context).withAlpha(204), WebTheme.getSurfaceColor(context), ], ), ), child: Center( child: SingleChildScrollView( physics: const NeverScrollableScrollPhysics(), child: Column( mainAxisSize: MainAxisSize.min, children: [ if (editorController.isLoadingMore) // Use passed controller Container( padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24), margin: const EdgeInsets.only(bottom: 8), decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( color: WebTheme.getShadowColor(context, opacity: 0.1), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(WebTheme.getPrimaryColor(context)), ), ), const SizedBox(width: 16), Text( '正在加载更多内容...', style: TextStyle( color: WebTheme.getTextColor(context), fontWeight: FontWeight.w500, fontSize: 16, ), ), ], ), ), if (!editorController.isLoadingMore) ...[ // Use passed controller if (editorController.hasReachedEnd) // Use passed controller _buildEndOfContentIndicator(context, '已到达底部'), if (editorController.hasReachedStart) // Use passed controller _buildEndOfContentIndicator(context, '已到达顶部'), ], ], ), ), ), ), ); } // 显示编辑器侧边栏宽度调整对话框 void _showEditorSidebarWidthDialog(BuildContext context) { final layoutState = Provider.of(context, listen: false); EditorDialogManager.showEditorSidebarWidthDialog( context, layoutState.editorSidebarWidth, EditorLayoutManager.minEditorSidebarWidth, EditorLayoutManager.maxEditorSidebarWidth, (value) { layoutState.editorSidebarWidth = value; }, layoutState.saveEditorSidebarWidth, ); } } /// 左侧侧边栏的精简抽屉,仅展示底部功能的精简版与展开按钮 class _CompactSidebarDrawer extends StatelessWidget { const _CompactSidebarDrawer({ required this.onExpand, required this.onOpenSettings, required this.onOpenAIChat, }); final VoidCallback onExpand; final VoidCallback onOpenSettings; final VoidCallback onOpenAIChat; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Material( color: WebTheme.getBackgroundColor(context), child: Column( children: [ // 顶部展开按钮 Padding( padding: const EdgeInsets.all(8.0), child: Tooltip( message: '展开侧边栏', child: InkWell( onTap: onExpand, borderRadius: BorderRadius.circular(8), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: colorScheme.outlineVariant.withValues(alpha: 0.5), width: 1, ), ), child: Icon(Icons.menu_open, size: 18, color: colorScheme.onSurfaceVariant), ), ), ), ), const Spacer(), // 精简功能按钮区:仅保留与底部栏一致的核心功能 _CompactActionButton( icon: Icons.settings, tooltip: '小说设置', onTap: onOpenSettings, ), const SizedBox(height: 8), _CompactActionButton( icon: Icons.chat_bubble_outline, tooltip: 'AI聊天', onTap: onOpenAIChat, ), const SizedBox(height: 8), _CompactActionButton( icon: Icons.lightbulb_outline, tooltip: '提示词', onTap: () { context.read(); // 使用 EditorAppBar 的提示词入口逻辑:通过 EditorController 切换提示词视图 final controller = Provider.of(context, listen: false); controller.togglePromptView(); }, ), const SizedBox(height: 8), _CompactActionButton( icon: Icons.save_outlined, tooltip: '保存', onTap: () { try { final controller = Provider.of(context, listen: false); controller.editorBloc.add(const editor_bloc.SaveContent()); } catch (_) {} }, ), const SizedBox(height: 12), ], ), ); } } class _CompactActionButton extends StatelessWidget { const _CompactActionButton({ required this.icon, required this.tooltip, required this.onTap, }); final IconData icon; final String tooltip; final VoidCallback onTap; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Tooltip( message: tooltip, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(8), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: colorScheme.outlineVariant.withValues(alpha: 0.5), width: 1, ), ), child: Icon(icon, size: 18, color: colorScheme.onSurfaceVariant), ), ), ); } }