import 'package:ainoval/blocs/editor/editor_bloc.dart'; import 'package:ainoval/models/novel_summary.dart'; import 'package:ainoval/models/novel_snippet.dart'; import 'package:ainoval/screens/editor/widgets/novel_setting_sidebar.dart'; import 'package:ainoval/screens/editor/widgets/snippet_list_tab.dart'; import 'package:ainoval/screens/editor/widgets/snippet_edit_form.dart'; import 'package:ainoval/utils/logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:ainoval/screens/editor/controllers/editor_screen_controller.dart'; import 'package:ainoval/blocs/setting/setting_bloc.dart'; import 'package:ainoval/utils/web_theme.dart'; import 'package:ainoval/widgets/common/user_avatar_menu.dart'; import 'package:ainoval/screens/subscription/subscription_screen.dart'; import 'chapter_directory_tab.dart'; /// 保持存活状态的包装器组件 class _KeepAliveWrapper extends StatefulWidget { final Widget child; const _KeepAliveWrapper({required this.child}); @override State<_KeepAliveWrapper> createState() => _KeepAliveWrapperState(); } class _KeepAliveWrapperState extends State<_KeepAliveWrapper> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); return widget.child; } } class EditorSidebar extends StatefulWidget { const EditorSidebar({ super.key, required this.novel, required this.tabController, this.onOpenAIChat, this.onOpenSettings, this.onToggleSidebar, this.onAdjustWidth, }); final NovelSummary novel; final TabController tabController; final VoidCallback? onOpenAIChat; final VoidCallback? onOpenSettings; final VoidCallback? onToggleSidebar; final VoidCallback? onAdjustWidth; @override State createState() => _EditorSidebarState(); } class _EditorSidebarState extends State { final TextEditingController _searchController = TextEditingController(); // String _selectedMode = 'codex'; // 片段列表操作回调 VoidCallback? _refreshSnippetList; // used via callbacks wiring Function(NovelSnippet)? _addSnippetToList; // used via callbacks wiring Function(NovelSnippet)? _updateSnippetInList; // used via callbacks wiring Function(String)? _removeSnippetFromList; // used via callbacks wiring String _selectedBottomBarItem = ''; @override void dispose() { _searchController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // 🚀 添加重建监控日志 - 现在应该不会频繁触发了 AppLogger.d('EditorSidebar', '🔄 EditorSidebar.build() 被调用 - 监控重建'); final theme = Theme.of(context); // 🚀 优化:直接使用父级提供的SettingBloc实例,避免重复创建 final settingSidebarWidget = BlocProvider.value( value: context.read(), child: NovelSettingSidebar(novelId: widget.novel.id), ); return Material( color: WebTheme.getBackgroundColor(context), child: Container( decoration: BoxDecoration( color: WebTheme.getBackgroundColor(context), border: Border( right: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), boxShadow: [ BoxShadow( color: theme.colorScheme.shadow.withOpacity(0.03), blurRadius: 5, offset: const Offset(0, 0), ), ], ), child: Column( children: [ // 顶部应用栏 _buildAppBar(theme), // 标签页导航 _buildTabBar(theme), // 标签页内容 Expanded( child: TabBarView( controller: widget.tabController, children: [ // 设定库标签页(替换原来的Codex标签页) settingSidebarWidget, // 片段标签页 Builder( builder: (context) { return SnippetListTab( key: ValueKey('snippet_list_${widget.novel.id}'), novel: widget.novel, onRefreshCallbackChanged: (callback) { _refreshSnippetList = callback; }, onAddSnippetCallbackChanged: (callback) { _addSnippetToList = callback; }, onUpdateSnippetCallbackChanged: (callback) { _updateSnippetInList = callback; }, onRemoveSnippetCallbackChanged: (callback) { _removeSnippetFromList = callback; }, onSnippetTap: (snippet) { FloatingSnippetEditor.show( context: context, snippet: snippet, onSaved: (updatedSnippet) { // 判断是创建还是更新 if (snippet.id.isEmpty) { // 创建新片段:直接添加到列表 _addSnippetToList?.call(updatedSnippet); } else { // 更新现有片段:更新列表中的片段 _updateSnippetInList?.call(updatedSnippet); } }, onDeleted: (snippetId) { // 删除片段:从列表中移除 _removeSnippetFromList?.call(snippetId); }, ); }, ); }, ), // 章节目录标签页 Builder( builder: (context) { // 确保在有Provider访问权限的新BuildContext中构建ChapterDirectoryTab return Consumer( builder: (context, controller, child) { return ChapterDirectoryTab(novel: widget.novel); }, ); }, ), // 添加AI生成选项 _buildPlaceholderTab( icon: Icons.auto_awesome, text: 'AI生成功能开发中'), ], ), ), // 底部导航栏 _buildBottomBar(theme), ], ), ), ); } PreferredSizeWidget _buildAppBar(ThemeData theme) { return AppBar( elevation: 0, scrolledUnderElevation: 0, backgroundColor: WebTheme.getBackgroundColor(context), automaticallyImplyLeading: false, titleSpacing: 0, toolbarHeight: 60, // 增加高度以适应新设计 title: Container( height: 60, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row( children: [ // 返回按钮 Container( width: 32, height: 32, decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(8), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: () { Navigator.pop(context); }, child: Icon( Icons.arrow_back, size: 18, color: WebTheme.getTextColor(context), ), ), ), ), const SizedBox(width: 12), // 可点击的设置和小说信息区域 Expanded( child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: widget.onOpenSettings, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( children: [ // 设置图标 Container( width: 28, height: 28, decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(6), ), child: Icon( Icons.settings, size: 16, color: WebTheme.getTextColor(context), ), ), const SizedBox(width: 12), // 小说标题和作者信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ Text( widget.novel.title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 15, color: WebTheme.getTextColor(context), height: 1.1, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), Text( widget.novel.author ?? 'Erminia Osteen', style: TextStyle( color: WebTheme.getSecondaryTextColor(context), fontSize: 11, fontWeight: FontWeight.w400, height: 1.0, ), overflow: TextOverflow.ellipsis, maxLines: 1, ), ], ), ), ], ), ), ), ), ), const SizedBox(width: 8), // 右侧操作按钮 Row( mainAxisSize: MainAxisSize.min, children: [ // 侧边栏折叠按钮 Container( width: 32, height: 32, decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(8), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: widget.onToggleSidebar, child: Icon( Icons.menu_open, size: 18, color: WebTheme.getTextColor(context), ), ), ), ), const SizedBox(width: 8), // 调整宽度按钮 Container( width: 32, height: 32, decoration: BoxDecoration( color: WebTheme.getSurfaceColor(context), borderRadius: BorderRadius.circular(8), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(8), onTap: widget.onAdjustWidth, child: Icon( Icons.more_horiz, size: 18, color: WebTheme.getTextColor(context), ), ), ), ), ], ), ], ), ), ); } Widget _buildTabBar(ThemeData theme) { return Container( decoration: BoxDecoration( color: WebTheme.getBackgroundColor(context), border: Border( bottom: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: TabBar( controller: widget.tabController, labelColor: WebTheme.getTextColor(context), unselectedLabelColor: WebTheme.getSecondaryTextColor(context), indicatorColor: WebTheme.getTextColor(context), indicatorWeight: 2.0, // 减小指示器粗细 indicatorSize: TabBarIndicatorSize.label, labelStyle: const TextStyle( fontWeight: FontWeight.w600, fontSize: 13, // 减小字体大小 ), unselectedLabelStyle: const TextStyle( fontWeight: FontWeight.normal, fontSize: 13, // 减小字体大小 ), dividerColor: Colors.transparent, isScrollable: false, // 确保不可滚动,平均分配空间 labelPadding: const EdgeInsets.symmetric(horizontal: 2.0), // 减小标签内边距 padding: const EdgeInsets.symmetric(horizontal: 2.0), // 减小整体内边距 tabs: const [ Tab( icon: Icon(Icons.inventory_2_outlined, size: 18), // 修改图标来反映设定功能 text: '设定库', // 改为"设定库" height: 60, // 与顶部 AppBar 高度一致 ), Tab( icon: Icon(Icons.bookmark_border_outlined, size: 18), // 减小图标大小 text: '片段', height: 60, // 与顶部 AppBar 高度一致 ), Tab( icon: Icon(Icons.menu_outlined, size: 18), // 目录图标 text: '章节目录', // "章节目录" height: 60, // 与顶部 AppBar 高度一致 ), Tab( icon: Icon(Icons.auto_awesome, size: 18), // AI生成图标 text: 'AI生成', height: 60, // 与顶部 AppBar 高度一致 ), ], ), ); } Widget _buildPlaceholderTab({required IconData icon, required String text}) { return _KeepAliveWrapper( child: Container( color: WebTheme.getSurfaceColor(context), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 48, color: WebTheme.getSecondaryTextColor(context)), const SizedBox(height: 16), Text( text, style: TextStyle(fontSize: 16, color: WebTheme.getSecondaryTextColor(context)), textAlign: TextAlign.center, ), ], ), ), ), ); } Widget _buildBottomBar(ThemeData theme) { return LayoutBuilder( builder: (context, constraints) { // 当侧边栏宽度较小时,仅显示图标;宽度充足时显示图标+文字 final bool isCompact = constraints.maxWidth < 240; return Container( height: 60, decoration: BoxDecoration( color: WebTheme.getBackgroundColor(context), border: Border( top: BorderSide( color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey200 : WebTheme.grey200, width: 1.0, ), ), ), child: Row( children: [ // 用户头像菜单 Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: UserAvatarMenu( size: 16, showName: false, onMySubscription: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const SubscriptionScreen()), ); }, onOpenSettings: widget.onOpenSettings, onProfile: widget.onOpenSettings, // 个人资料也使用设置面板 onAccountSettings: widget.onOpenSettings, // 账户设置使用设置面板 ), ), // 使用Expanded包裹SingleChildScrollView来确保按钮能够根据宽度滚动/自适应 Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ // 帮助按钮 _buildBottomBarItem( icon: Icons.help_outline, label: 'Help', showLabel: !isCompact, selected: _selectedBottomBarItem == 'Help', onTap: () { setState(() { _selectedBottomBarItem = 'Help'; }); // TODO: 实现帮助功能 }, ), // 提示按钮 _buildBottomBarItem( icon: Icons.lightbulb_outline, label: 'Prompts', showLabel: !isCompact, selected: _selectedBottomBarItem == 'Prompts', onTap: () { setState(() { _selectedBottomBarItem = 'Prompts'; }); final controller = Provider.of(context, listen: false); controller.togglePromptView(); }, ), // 导出按钮 _buildBottomBarItem( icon: Icons.download_outlined, label: 'Export', showLabel: !isCompact, selected: _selectedBottomBarItem == 'Export', onTap: () { setState(() { _selectedBottomBarItem = 'Export'; }); // TODO: 实现导出功能 }, ), // 保存按钮 _buildBottomBarItem( icon: Icons.save_outlined, label: 'Save', showLabel: !isCompact, selected: _selectedBottomBarItem == 'Save', onTap: () { setState(() { _selectedBottomBarItem = 'Save'; }); // 手动保存:触发与自动保存一致的SaveContent事件 try { final controller = Provider.of(context, listen: false); controller.editorBloc.add(const SaveContent()); } catch (e) { AppLogger.w('EditorSidebar', '手动保存触发失败', e); } }, ), ], ), ), ), ], ), ); }, ); } /// 构建底部栏单个按钮 Widget _buildBottomBarItem({ required IconData icon, required String label, bool showLabel = true, bool selected = false, required VoidCallback onTap, }) { final isDark = WebTheme.isDarkMode(context); // 修复选中状态的颜色配置,确保在暗黑模式下文字可见 final Color foregroundColor; final Color backgroundColor; if (selected) { if (isDark) { // 暗黑模式下:选中时使用深灰背景+白字 backgroundColor = WebTheme.darkGrey700; foregroundColor = WebTheme.white; } else { // 亮色模式下:选中时使用深色背景+白字 backgroundColor = WebTheme.grey800; foregroundColor = WebTheme.white; } } else { // 未选中时:透明背景+半透明文字 backgroundColor = Colors.transparent; foregroundColor = WebTheme.getTextColor(context).withOpacity(0.7); } return Material( color: backgroundColor, borderRadius: BorderRadius.circular(6), child: InkWell( borderRadius: BorderRadius.circular(6), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10.0), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 16, color: foregroundColor, ), if (showLabel) ...[ const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 12, color: foregroundColor, fontWeight: FontWeight.w500, ), ), ], ], ), ), ), ); } } class _CodexEmptyState extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, // 左对齐 children: [ Text( 'YOUR CODEX IS EMPTY', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.5, color: WebTheme.getTextColor(context), ), ), const SizedBox(height: 8), Text( 'The Codex stores information about the world your story takes place in, its inhabitants and more.', style: TextStyle( color: WebTheme.getSecondaryTextColor(context), fontSize: 14, height: 1.5, ), ), const SizedBox(height: 12), InkWell( onTap: () { // 该点击应执行与"+ New Entry"按钮相同的操作 }, child: Text( '→ Create a new entry by clicking the button above.', style: TextStyle( color: WebTheme.getTextColor(context), fontSize: 14, fontWeight: FontWeight.w500, ), ), ), ], ), ); } }