马良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,802 @@
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<EditorScreenController>(
builder: (context, editorController, _) {
// 主要布局始终在Stack中
Widget mainContent;
if (editorController.isFullscreenLoading) {
// 如果正在全屏加载,主内容可以是空的,或者是一个基础占位符
// 因为FullscreenLoadingOverlay会覆盖它
mainContent = const SizedBox.shrink();
} else {
// 正常的主布局
mainContent = ValueListenableBuilder<String>(
valueListenable: stateManager.contentUpdateNotifier,
builder: (context, updateValue, child) {
return BlocBuilder<editor_bloc.EditorBloc, editor_bloc.EditorState>(
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<EditorLayoutManager>(
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<EditorLayoutManager>().toggleAIChatSidebar();
},
onOpenSettings: () {
context.read<EditorLayoutManager>().toggleNovelSettings();
},
onToggleSidebar: () {
context.read<EditorLayoutManager>().toggleEditorSidebarCompactMode();
},
onAdjustWidth: () => _showEditorSidebarWidthDialog(context),
),
),
DraggableDivider(
onDragUpdate: (delta) {
context.read<EditorLayoutManager>().updateEditorSidebarWidth(delta.delta.dx);
},
onDragEnd: (_) {
context.read<EditorLayoutManager>().saveEditorSidebarWidth();
},
),
],
);
},
);
},
),
// 主编辑区域 - 完全不监听EditorLayoutManager的变化
Expanded(
child: Column(
children: [
// 编辑器顶部工具栏和操作栏
BlocBuilder<editor_bloc.EditorBloc, editor_bloc.EditorState>(
buildWhen: (prev, curr) => curr is editor_bloc.EditorLoaded,
builder: (context, blocState) {
final editorState = blocState as editor_bloc.EditorLoaded;
return Consumer<EditorLayoutManager>(
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<isEditorSidebarVisible>),这里不再添加
// 实现方式:在 _buildOverlayWidgets 内部保留原实现,这里不使用第一个返回项
return true;
}),
// 小屏右侧AI面板覆盖式展示
_buildRightPanelOverlayIfNeeded(context, editorBlocState, editorController, isNarrow: isNarrow),
],
);
}
// 构建主编辑器内容不包含右侧AI面板
Widget _buildMainEditorContentOnly(BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController) {
// 主编辑器内容区域 - 监听小说设置状态变化
return Selector<EditorLayoutManager, bool>(
selector: (context, layoutManager) => layoutManager.isNovelSettingsVisible,
builder: (context, isNovelSettingsVisible, child) {
if (isNovelSettingsVisible) {
return MultiRepositoryProvider(
providers: [
RepositoryProvider<EditorRepository>(
create: (context) => editorController.editorRepository,
),
RepositoryProvider<StorageRepository>(
create: (context) => AliyunOssStorageRepository(editorController.apiClient),
),
],
child: NovelSettingsView(
novel: editorController.novel,
onSettingsClose: () {
context.read<EditorLayoutManager>().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<EditorLayoutManager>(
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<PromptRepository>(
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<EditorLayoutManager>(
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<PromptRepository>(
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<Widget> _buildOverlayWidgets(BuildContext context, editor_bloc.EditorLoaded editorBlocState, EditorScreenController editorController, EditorStateManager stateManager) {
return [
// 移除:不再提供“完全隐藏侧边栏”的开关按钮,保留其他覆盖层
// 设置面板
Selector<EditorLayoutManager, bool>(
selector: (context, layoutManager) => layoutManager.isSettingsPanelVisible,
builder: (context, isVisible, child) {
if (!isVisible) return const SizedBox.shrink();
return Positioned.fill(
child: GestureDetector(
onTap: () => context.read<EditorLayoutManager>().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<EditorLayoutManager>().toggleSettingsPanel(),
)
: SettingsPanel(
stateManager: stateManager,
userId: editorController.currentUserId!,
onClose: () => context.read<EditorLayoutManager>().toggleSettingsPanel(),
editorSettings: EditorSettings.fromMap(editorBlocState.settings),
onEditorSettingsChanged: (settings) {
context.read<editor_bloc.EditorBloc>().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<Color>(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<Color>(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<EditorLayoutManager>(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<editor_bloc.EditorBloc>();
// 使用 EditorAppBar 的提示词入口逻辑:通过 EditorController 切换提示词视图
final controller = Provider.of<EditorScreenController>(context, listen: false);
controller.togglePromptView();
},
),
const SizedBox(height: 8),
_CompactActionButton(
icon: Icons.save_outlined,
tooltip: '保存',
onTap: () {
try {
final controller = Provider.of<EditorScreenController>(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),
),
),
);
}
}