1527 lines
48 KiB
Dart
1527 lines
48 KiB
Dart
import 'package:ainoval/blocs/editor/editor_bloc.dart' as editor;
|
||
import 'package:ainoval/models/novel_structure.dart' as novel_models;
|
||
import 'package:ainoval/utils/logger.dart';
|
||
import 'package:ainoval/utils/web_theme.dart';
|
||
import 'package:ainoval/components/editable_title.dart';
|
||
import 'package:ainoval/screens/editor/widgets/menu_builder.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||
|
||
/// 大纲视图组件 - 显示小说的整体结构和各场景摘要
|
||
/// 支持Act、Chapter、Scene的层级管理和编辑功能
|
||
/// 🚀 重构:现在使用EditorBloc统一管理数据,提供无感刷新功能
|
||
class PlanView extends StatefulWidget {
|
||
const PlanView({
|
||
super.key,
|
||
required this.novelId,
|
||
required this.editorBloc, // 🚀 修改:使用EditorBloc替代PlanBloc
|
||
this.onSwitchToWrite,
|
||
});
|
||
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc; // 🚀 修改:改为EditorBloc
|
||
final VoidCallback? onSwitchToWrite;
|
||
|
||
@override
|
||
State<PlanView> createState() => _PlanViewState();
|
||
}
|
||
|
||
class _PlanViewState extends State<PlanView> {
|
||
final ScrollController _scrollController = ScrollController();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
// 🚀 修改:使用EditorBloc的事件
|
||
widget.editorBloc.add(const editor.SwitchToPlanView());
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_scrollController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return WebTheme.getMaterialWrapper(
|
||
child: BlocBuilder<editor.EditorBloc, editor.EditorState>(
|
||
bloc: widget.editorBloc,
|
||
builder: (context, state) {
|
||
// 🚀 修改:处理EditorState而不是PlanState
|
||
if (state is! editor.EditorLoaded) {
|
||
return Center(
|
||
child: CircularProgressIndicator(color: WebTheme.getPrimaryColor(context)),
|
||
);
|
||
}
|
||
|
||
final editorState = state;
|
||
|
||
// 显示错误信息
|
||
if (editorState.errorMessage != null) {
|
||
return Center(
|
||
child: Text(
|
||
'加载失败: ${editorState.errorMessage}',
|
||
style: TextStyle(color: WebTheme.getTextColor(context)),
|
||
),
|
||
);
|
||
}
|
||
|
||
final novel = editorState.novel;
|
||
|
||
return Container(
|
||
// 使用动态背景色,兼容明暗主题
|
||
color: WebTheme.getSurfaceColor(context),
|
||
child: Column(
|
||
children: [
|
||
// 主要内容区 - 使用完全虚拟化的滚动
|
||
Expanded(
|
||
child: _VirtualizedPlanView(
|
||
novel: novel,
|
||
novelId: widget.novelId,
|
||
editorBloc: widget.editorBloc,
|
||
onSwitchToWrite: widget.onSwitchToWrite,
|
||
scrollController: _scrollController,
|
||
),
|
||
),
|
||
// 底部工具栏
|
||
_PlanToolbar(editorBloc: widget.editorBloc), // 🚀 修改:传递EditorBloc
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 已弃用:_ActSection(被虚拟化布局替代)
|
||
|
||
/// Act标题头部组件
|
||
class _ActHeader extends StatelessWidget {
|
||
const _ActHeader({
|
||
required this.act,
|
||
required this.novelId,
|
||
required this.editorBloc, // 🚀 修改:使用EditorBloc
|
||
});
|
||
|
||
final novel_models.Act act;
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc; // 🚀 修改:改为EditorBloc
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
// 折叠按钮
|
||
IconButton(
|
||
icon: Icon(Icons.keyboard_arrow_down, size: 18, color: WebTheme.getSecondaryTextColor(context)),
|
||
onPressed: () {
|
||
// TODO(plan): 实现折叠功能
|
||
},
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
|
||
),
|
||
// Act标题(可编辑)
|
||
Expanded(
|
||
child: EditableTitle(
|
||
initialText: act.title,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
color: WebTheme.getTextColor(context),
|
||
),
|
||
// 仅在提交(回车或失焦)时派发更新
|
||
onSubmitted: (value) {
|
||
editorBloc.add(editor.UpdateActTitle(
|
||
actId: act.id,
|
||
title: value,
|
||
));
|
||
},
|
||
),
|
||
),
|
||
// 添加章节按钮
|
||
_SmallIconButton(
|
||
icon: Icons.add,
|
||
tooltip: '添加章节',
|
||
onPressed: () {
|
||
// 🚀 修改:使用EditorBloc事件
|
||
editorBloc.add(editor.AddNewChapter(
|
||
novelId: novelId,
|
||
actId: act.id,
|
||
));
|
||
},
|
||
),
|
||
const SizedBox(width: 4),
|
||
// 更多操作菜单(统一下拉样式)
|
||
MenuBuilder.buildActMenu(
|
||
context: context,
|
||
editorBloc: editorBloc,
|
||
actId: act.id,
|
||
onRenamePressed: null,
|
||
width: 220,
|
||
align: 'right',
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 章节卡片组件 - 自适应高度显示章节及其场景
|
||
// 已弃用:_ChapterCard(使用 _OptimizedChapterCard 取代)
|
||
|
||
/// 章节标题头部
|
||
class _ChapterHeader extends StatelessWidget {
|
||
const _ChapterHeader({
|
||
required this.actId,
|
||
required this.chapter,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final String actId;
|
||
final novel_models.Chapter chapter;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 计算总字数
|
||
final totalWords = chapter.scenes.fold<int>(
|
||
0,
|
||
(sum, scene) => sum + (scene.content.length),
|
||
);
|
||
|
||
return Container(
|
||
height: 30, // 🚀 修改:设置固定高度,章节头部缩短为原来的三分之一
|
||
padding: const EdgeInsets.fromLTRB(8, 0, 4, 0), // 🚀 修改:去掉垂直内边距,使用固定高度
|
||
decoration: BoxDecoration(
|
||
border: Border(bottom: BorderSide(color: WebTheme.grey200)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
// 拖拽手柄
|
||
Icon(Icons.drag_indicator, size: 14, color: WebTheme.getSecondaryTextColor(context)),
|
||
const SizedBox(width: 6),
|
||
// 章节标题(可编辑)
|
||
Expanded(
|
||
child: EditableTitle(
|
||
initialText: chapter.title,
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: WebTheme.getTextColor(context),
|
||
),
|
||
// 仅在提交(回车或失焦)时派发更新
|
||
onSubmitted: (value) {
|
||
editorBloc.add(editor.UpdateChapterTitle(
|
||
actId: actId,
|
||
chapterId: chapter.id,
|
||
title: value,
|
||
));
|
||
},
|
||
),
|
||
),
|
||
// 字数统计
|
||
if (totalWords > 0) ...[
|
||
Text(
|
||
'$totalWords Words',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
],
|
||
// 编辑按钮
|
||
_SmallIconButton(
|
||
icon: Icons.edit,
|
||
tooltip: '编辑章节',
|
||
onPressed: () {
|
||
_showEditDialog(
|
||
context: context,
|
||
title: '编辑章节标题',
|
||
initialValue: chapter.title,
|
||
onSave: (newTitle) {
|
||
editorBloc.add(editor.UpdateChapterTitle(
|
||
actId: actId,
|
||
chapterId: chapter.id,
|
||
title: newTitle,
|
||
));
|
||
},
|
||
);
|
||
},
|
||
),
|
||
// 更多操作(统一下拉样式)
|
||
MenuBuilder.buildChapterMenu(
|
||
context: context,
|
||
editorBloc: editorBloc,
|
||
actId: actId,
|
||
chapterId: chapter.id,
|
||
onRenamePressed: null,
|
||
width: 220,
|
||
align: 'right',
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 场景项组件 - 单个场景的显示和交互
|
||
class _SceneItem extends StatefulWidget {
|
||
const _SceneItem({
|
||
required this.actId,
|
||
required this.chapterId,
|
||
required this.scene,
|
||
required this.sceneNumber,
|
||
required this.novelId,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final String actId;
|
||
final String chapterId;
|
||
final novel_models.Scene scene;
|
||
final int sceneNumber;
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
State<_SceneItem> createState() => _SceneItemState();
|
||
}
|
||
|
||
class _SceneItemState extends State<_SceneItem> {
|
||
late TextEditingController _summaryController;
|
||
bool _isEditing = true;
|
||
bool _hasUnsavedChanges = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_summaryController = TextEditingController(text: widget.scene.summary.content);
|
||
_summaryController.addListener(_onSummaryChanged);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_summaryController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _onSummaryChanged() {
|
||
final hasChanges = _summaryController.text != widget.scene.summary.content;
|
||
if (hasChanges != _hasUnsavedChanges) {
|
||
setState(() {
|
||
_hasUnsavedChanges = hasChanges;
|
||
});
|
||
}
|
||
}
|
||
|
||
void _saveSummary() {
|
||
if (_hasUnsavedChanges) {
|
||
// 🚀 修改:使用EditorBloc的UpdateSummary事件
|
||
widget.editorBloc.add(editor.UpdateSummary(
|
||
novelId: widget.novelId,
|
||
actId: widget.actId,
|
||
chapterId: widget.chapterId,
|
||
sceneId: widget.scene.id,
|
||
summary: _summaryController.text,
|
||
));
|
||
setState(() {
|
||
_hasUnsavedChanges = false;
|
||
_isEditing = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
void _navigateToScene() {
|
||
AppLogger.i('PlanView', '准备跳转到场景: ${widget.actId} - ${widget.chapterId} - ${widget.scene.id}');
|
||
|
||
// 🚀 修改:使用EditorBloc的NavigateToSceneFromPlan事件
|
||
widget.editorBloc.add(editor.NavigateToSceneFromPlan(
|
||
actId: widget.actId,
|
||
chapterId: widget.chapterId,
|
||
sceneId: widget.scene.id,
|
||
));
|
||
|
||
Future.delayed(const Duration(milliseconds: 300), () {
|
||
// 跳转后可在外部触发切换
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final hasContent = widget.scene.summary.content.isNotEmpty;
|
||
final wordCount = widget.scene.content.length;
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : WebTheme.grey200),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 工具栏区域 - 动态背景
|
||
Container(
|
||
height: 27, // 🚀 修改:设置固定高度,场景头部比章节头部稍小
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey100 : WebTheme.grey50,
|
||
borderRadius: const BorderRadius.only(
|
||
topLeft: Radius.circular(6),
|
||
topRight: Radius.circular(6),
|
||
),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), // 🚀 修改:去掉垂直内边距,使用固定高度
|
||
child: Row(
|
||
children: [
|
||
// 拖拽手柄
|
||
Icon(Icons.drag_indicator, size: 12, color: WebTheme.getSecondaryTextColor(context)),
|
||
const SizedBox(width: 4),
|
||
|
||
// 场景标签
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
child: Text(
|
||
'Scene ${widget.sceneNumber}',
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w600,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(width: 8),
|
||
|
||
// 字数统计(如果有)
|
||
if (wordCount > 0) ...[
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getPrimaryColor(context).withOpacity(0.05),
|
||
borderRadius: BorderRadius.circular(2),
|
||
border: Border.all(color: WebTheme.getPrimaryColor(context).withOpacity(0.2), width: 0.5),
|
||
),
|
||
child: Text(
|
||
'$wordCount Words',
|
||
style: TextStyle(
|
||
fontSize: 9,
|
||
color: WebTheme.getPrimaryColor(context),
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
],
|
||
|
||
const Spacer(),
|
||
|
||
// 保存指示器
|
||
if (_hasUnsavedChanges) ...[
|
||
Container(
|
||
width: 6,
|
||
height: 6,
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.warning,
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
GestureDetector(
|
||
onTap: _saveSummary,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.success,
|
||
borderRadius: BorderRadius.circular(3),
|
||
),
|
||
child: Text(
|
||
'保存',
|
||
style: TextStyle(
|
||
fontSize: 9,
|
||
color: WebTheme.isDarkMode(context) ? Colors.white : Colors.white,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
],
|
||
|
||
// 跳转按钮
|
||
_SmallIconButton(
|
||
icon: Icons.launch,
|
||
size: 12,
|
||
tooltip: '跳转到场景',
|
||
onPressed: _navigateToScene,
|
||
),
|
||
|
||
const SizedBox(width: 4),
|
||
|
||
// 编辑切换按钮
|
||
_SmallIconButton(
|
||
icon: _isEditing ? Icons.visibility : Icons.edit,
|
||
size: 12,
|
||
tooltip: _isEditing ? '预览模式' : '编辑模式',
|
||
onPressed: () {
|
||
setState(() {
|
||
_isEditing = !_isEditing;
|
||
});
|
||
},
|
||
),
|
||
|
||
const SizedBox(width: 4),
|
||
|
||
// 更多操作菜单
|
||
PopupMenuButton<String>(
|
||
icon: const Icon(Icons.more_vert, size: 12, color: Colors.black54),
|
||
padding: EdgeInsets.zero,
|
||
constraints: const BoxConstraints(minWidth: 16, minHeight: 16),
|
||
offset: const Offset(-40, 16),
|
||
elevation: 2,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||
itemBuilder: (context) => [
|
||
const PopupMenuItem(
|
||
value: 'delete',
|
||
height: 30,
|
||
child: Row(
|
||
children: [
|
||
Icon(Icons.delete, size: 12, color: Colors.red),
|
||
SizedBox(width: 6),
|
||
Text('删除场景', style: TextStyle(fontSize: 11, color: Colors.red)),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
onSelected: (value) {
|
||
if (value == 'delete') {
|
||
_showDeleteDialog(
|
||
context: context,
|
||
title: '删除场景',
|
||
content: '确定要删除此场景吗?',
|
||
onConfirm: () {
|
||
widget.editorBloc.add(editor.DeleteScene(
|
||
novelId: widget.novelId,
|
||
actId: widget.actId,
|
||
chapterId: widget.chapterId,
|
||
sceneId: widget.scene.id,
|
||
));
|
||
},
|
||
);
|
||
}
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 摘要内容区域 - 动态背景,支持直接编辑
|
||
Container(
|
||
width: double.infinity,
|
||
constraints: const BoxConstraints(
|
||
minHeight: 220,
|
||
),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: const BorderRadius.only(
|
||
bottomLeft: Radius.circular(6),
|
||
bottomRight: Radius.circular(6),
|
||
),
|
||
),
|
||
child: _isEditing
|
||
? WebTheme.getMaterialWrapper(
|
||
child: TextField(
|
||
controller: _summaryController,
|
||
decoration: WebTheme.getBorderlessInputDecoration(
|
||
hintText: '输入场景摘要...',
|
||
context: context,
|
||
),
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getTextColor(context),
|
||
height: 1.8,
|
||
),
|
||
maxLines: null,
|
||
minLines: 5,
|
||
onSubmitted: (_) => _saveSummary(),
|
||
),
|
||
)
|
||
: GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_isEditing = true;
|
||
});
|
||
},
|
||
child: Container(
|
||
width: double.infinity,
|
||
child: hasContent
|
||
? Text(
|
||
widget.scene.summary.content,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getTextColor(context),
|
||
height: 1.8,
|
||
),
|
||
)
|
||
: Text(
|
||
'点击这里添加场景描述...',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
fontStyle: FontStyle.italic,
|
||
height: 1.8,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
|
||
// 底部按钮区域 - 浅灰色背景
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: const BorderRadius.only(
|
||
bottomLeft: Radius.circular(6),
|
||
bottomRight: Radius.circular(6),
|
||
),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
child: Row(
|
||
children: [
|
||
_SmallButton(
|
||
icon: Icons.add,
|
||
label: 'Codex',
|
||
onPressed: () {
|
||
// TODO(plan): 添加Codex功能
|
||
},
|
||
),
|
||
const SizedBox(width: 8),
|
||
_SmallButton(
|
||
icon: Icons.label,
|
||
label: 'Label',
|
||
onPressed: () {
|
||
// TODO(plan): 添加标签功能
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 添加场景按钮
|
||
class _AddSceneButton extends StatelessWidget {
|
||
const _AddSceneButton({
|
||
required this.actId,
|
||
required this.chapterId,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final String actId;
|
||
final String chapterId;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return SizedBox(
|
||
width: double.infinity,
|
||
child: OutlinedButton.icon(
|
||
icon: Icon(Icons.add, size: 14, color: WebTheme.getSecondaryTextColor(context)),
|
||
label: Text(
|
||
'New Scene',
|
||
style: TextStyle(fontSize: 12, color: WebTheme.getSecondaryTextColor(context)),
|
||
),
|
||
style: OutlinedButton.styleFrom(
|
||
side: BorderSide(color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : Colors.grey.shade300),
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
|
||
),
|
||
onPressed: () {
|
||
editorBloc.add(editor.AddNewScene(
|
||
novelId: '',
|
||
actId: actId,
|
||
chapterId: chapterId,
|
||
sceneId: 'scene_${DateTime.now().millisecondsSinceEpoch}',
|
||
));
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 添加章节卡片
|
||
class _AddChapterCard extends StatelessWidget {
|
||
const _AddChapterCard({
|
||
required this.actId,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final String actId;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
height: 200,
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : Colors.grey.shade300,
|
||
style: BorderStyle.solid,
|
||
),
|
||
),
|
||
child: Material(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: BorderRadius.circular(8),
|
||
child: InkWell(
|
||
borderRadius: BorderRadius.circular(8),
|
||
onTap: () {
|
||
editorBloc.add(editor.AddNewChapter(
|
||
novelId: '',
|
||
actId: actId,
|
||
));
|
||
},
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.add_circle_outline, size: 28, color: WebTheme.getSecondaryTextColor(context)),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'新章节',
|
||
style: TextStyle(fontSize: 13, color: WebTheme.getSecondaryTextColor(context)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 已弃用:_LazyChapterGrid(被虚拟化布局替代)
|
||
|
||
// 已弃用:_LazyWrapLayout(被虚拟化布局替代)
|
||
|
||
/// 添加Act按钮
|
||
class _AddActButton extends StatelessWidget {
|
||
const _AddActButton({required this.editorBloc});
|
||
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Center(
|
||
child: OutlinedButton.icon(
|
||
icon: Icon(Icons.add, color: WebTheme.getSecondaryTextColor(context)),
|
||
label: Text(
|
||
'添加新Act',
|
||
style: TextStyle(color: WebTheme.getSecondaryTextColor(context)),
|
||
),
|
||
style: OutlinedButton.styleFrom(
|
||
side: BorderSide(color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey400 : Colors.grey.shade400),
|
||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||
),
|
||
onPressed: () {
|
||
editorBloc.add(const editor.AddNewAct());
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 底部工具栏
|
||
class _PlanToolbar extends StatelessWidget {
|
||
const _PlanToolbar({required this.editorBloc});
|
||
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||
decoration: BoxDecoration(
|
||
// 使用动态背景色,兼容暗黑 / 亮色
|
||
color: WebTheme.getSurfaceColor(context),
|
||
border: Border(
|
||
top: BorderSide(
|
||
color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : Colors.grey.shade200,
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
_ToolbarButton(
|
||
icon: Icons.add_box_outlined,
|
||
label: '添加Act',
|
||
onPressed: () => editorBloc.add(const editor.AddNewAct()),
|
||
),
|
||
const SizedBox(width: 12),
|
||
_ToolbarButton(
|
||
icon: Icons.format_list_numbered,
|
||
label: '大纲设置',
|
||
onPressed: () {
|
||
// TODO(plan): 实现大纲设置
|
||
},
|
||
),
|
||
const SizedBox(width: 12),
|
||
_ToolbarButton(
|
||
icon: Icons.filter_alt_outlined,
|
||
label: '筛选',
|
||
onPressed: () {
|
||
// TODO(plan): 实现筛选功能
|
||
},
|
||
),
|
||
const SizedBox(width: 12),
|
||
_ToolbarButton(
|
||
icon: Icons.settings_outlined,
|
||
label: '选项',
|
||
onPressed: () {
|
||
// TODO(plan): 实现选项功能
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 通用小图标按钮组件
|
||
class _SmallIconButton extends StatelessWidget {
|
||
const _SmallIconButton({
|
||
required this.icon,
|
||
required this.onPressed,
|
||
this.tooltip,
|
||
this.size = 14,
|
||
});
|
||
|
||
final IconData icon;
|
||
final VoidCallback onPressed;
|
||
final String? tooltip;
|
||
final double size;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final button = IconButton(
|
||
icon: Icon(icon, size: size, color: WebTheme.getSecondaryTextColor(context)),
|
||
onPressed: onPressed,
|
||
padding: EdgeInsets.zero,
|
||
constraints: BoxConstraints(minWidth: size + 8, minHeight: size + 8),
|
||
);
|
||
|
||
return tooltip != null
|
||
? Tooltip(message: tooltip!, child: button)
|
||
: button;
|
||
}
|
||
}
|
||
|
||
/// 通用小按钮组件
|
||
class _SmallButton extends StatelessWidget {
|
||
const _SmallButton({
|
||
required this.icon,
|
||
required this.label,
|
||
required this.onPressed,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String label;
|
||
final VoidCallback onPressed;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return OutlinedButton.icon(
|
||
icon: Icon(icon, size: 12, color: WebTheme.getSecondaryTextColor(context)),
|
||
label: Text(
|
||
label,
|
||
style: TextStyle(fontSize: 10, color: WebTheme.getSecondaryTextColor(context)),
|
||
),
|
||
style: OutlinedButton.styleFrom(
|
||
side: BorderSide(color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : Colors.grey.shade300),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
minimumSize: const Size(0, 24),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||
),
|
||
onPressed: onPressed,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 工具栏按钮组件
|
||
class _ToolbarButton extends StatelessWidget {
|
||
const _ToolbarButton({
|
||
required this.icon,
|
||
required this.label,
|
||
required this.onPressed,
|
||
});
|
||
|
||
final IconData icon;
|
||
final String label;
|
||
final VoidCallback onPressed;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return TextButton.icon(
|
||
icon: Icon(icon, size: 16, color: WebTheme.getSecondaryTextColor(context)),
|
||
label: Text(
|
||
label,
|
||
style: TextStyle(fontSize: 13, color: WebTheme.getSecondaryTextColor(context)),
|
||
),
|
||
onPressed: onPressed,
|
||
style: TextButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 显示编辑对话框的通用函数
|
||
void _showEditDialog({
|
||
required BuildContext context,
|
||
required String title,
|
||
required String initialValue,
|
||
required Function(String) onSave,
|
||
}) {
|
||
final controller = TextEditingController(text: initialValue);
|
||
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
backgroundColor: WebTheme.getSurfaceColor(context),
|
||
title: Text(title, style: const TextStyle(fontSize: 16)),
|
||
content: TextField(
|
||
controller: controller,
|
||
decoration: InputDecoration(
|
||
border: OutlineInputBorder(
|
||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outlineVariant),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderSide: BorderSide(color: WebTheme.getPrimaryColor(context)),
|
||
),
|
||
),
|
||
autofocus: true,
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('取消', style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
if (controller.text.trim().isNotEmpty) {
|
||
onSave(controller.text.trim());
|
||
Navigator.pop(context);
|
||
}
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: WebTheme.getPrimaryColor(context),
|
||
foregroundColor: WebTheme.white,
|
||
),
|
||
child: const Text('保存'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 显示删除确认对话框的通用函数
|
||
void _showDeleteDialog({
|
||
required BuildContext context,
|
||
required String title,
|
||
required String content,
|
||
required VoidCallback onConfirm,
|
||
}) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
backgroundColor: WebTheme.getSurfaceColor(context),
|
||
title: Text(title, style: const TextStyle(fontSize: 16)),
|
||
content: Text(content, style: const TextStyle(fontSize: 14)),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('取消', style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: () {
|
||
Navigator.pop(context);
|
||
onConfirm();
|
||
},
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Theme.of(context).colorScheme.error,
|
||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||
),
|
||
child: const Text('删除'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 完全虚拟化的Plan视图 - 极致性能优化
|
||
class _VirtualizedPlanView extends StatelessWidget {
|
||
const _VirtualizedPlanView({
|
||
required this.novel,
|
||
required this.novelId,
|
||
required this.editorBloc,
|
||
this.onSwitchToWrite,
|
||
required this.scrollController,
|
||
});
|
||
|
||
final novel_models.Novel novel;
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc;
|
||
final VoidCallback? onSwitchToWrite;
|
||
final ScrollController scrollController;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// 将所有内容展平为一个线性列表,实现真正的虚拟化滚动
|
||
final List<_PlanItem> items = _buildFlatItemList();
|
||
|
||
return CustomScrollView(
|
||
controller: scrollController,
|
||
cacheExtent: 200.0, // 减少缓存范围,提高性能
|
||
slivers: [
|
||
SliverPadding(
|
||
padding: const EdgeInsets.all(16.0),
|
||
sliver: SliverList(
|
||
delegate: SliverChildBuilderDelegate(
|
||
(context, index) {
|
||
if (index >= items.length) return null;
|
||
|
||
final item = items[index];
|
||
return _buildItemWidget(context, item);
|
||
},
|
||
childCount: items.length,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// 构建展平的项目列表
|
||
List<_PlanItem> _buildFlatItemList() {
|
||
final List<_PlanItem> items = [];
|
||
|
||
for (int actIndex = 0; actIndex < novel.acts.length; actIndex++) {
|
||
final act = novel.acts[actIndex];
|
||
|
||
// 添加Act标题项
|
||
items.add(_PlanItem(
|
||
type: _PlanItemType.actHeader,
|
||
act: act,
|
||
actIndex: actIndex,
|
||
));
|
||
|
||
// 添加章节项(分批处理,每批最多10个章节)
|
||
const int batchSize = 10;
|
||
for (int batchStart = 0; batchStart < act.chapters.length; batchStart += batchSize) {
|
||
final batchEnd = (batchStart + batchSize).clamp(0, act.chapters.length);
|
||
final batchChapters = act.chapters.sublist(batchStart, batchEnd);
|
||
|
||
items.add(_PlanItem(
|
||
type: _PlanItemType.chapterBatch,
|
||
act: act,
|
||
chapters: batchChapters,
|
||
batchStart: batchStart,
|
||
));
|
||
}
|
||
|
||
// 添加"添加章节"按钮
|
||
items.add(_PlanItem(
|
||
type: _PlanItemType.addChapter,
|
||
act: act,
|
||
));
|
||
}
|
||
|
||
// 添加"添加Act"按钮
|
||
items.add(_PlanItem(
|
||
type: _PlanItemType.addAct,
|
||
));
|
||
|
||
return items;
|
||
}
|
||
|
||
/// 构建单个项目的Widget
|
||
Widget _buildItemWidget(BuildContext context, _PlanItem item) {
|
||
switch (item.type) {
|
||
case _PlanItemType.actHeader:
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 12.0),
|
||
child: _ActHeader(
|
||
act: item.act!,
|
||
novelId: novelId,
|
||
editorBloc: editorBloc,
|
||
),
|
||
);
|
||
|
||
case _PlanItemType.chapterBatch:
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 16.0, left: 16.0),
|
||
child: _ChapterBatchWidget(
|
||
act: item.act!,
|
||
chapters: item.chapters!,
|
||
novelId: novelId,
|
||
editorBloc: editorBloc,
|
||
),
|
||
);
|
||
|
||
case _PlanItemType.addChapter:
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 16.0, left: 16.0),
|
||
child: SizedBox(
|
||
width: 450,
|
||
child: _AddChapterCard(
|
||
actId: item.act!.id,
|
||
editorBloc: editorBloc,
|
||
),
|
||
),
|
||
);
|
||
|
||
case _PlanItemType.addAct:
|
||
return Padding(
|
||
padding: const EdgeInsets.only(top: 16.0),
|
||
child: _AddActButton(editorBloc: editorBloc),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 章节批次Widget - 一次显示一批章节
|
||
class _ChapterBatchWidget extends StatelessWidget {
|
||
const _ChapterBatchWidget({
|
||
required this.act,
|
||
required this.chapters,
|
||
required this.novelId,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final novel_models.Act act;
|
||
final List<novel_models.Chapter> chapters;
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
// 计算每行可以放多少个卡片
|
||
const itemWidth = 450.0;
|
||
const spacing = 16.0;
|
||
final availableWidth = constraints.maxWidth;
|
||
final itemsPerRow = ((availableWidth + spacing) / (itemWidth + spacing)).floor().clamp(1, 10);
|
||
|
||
// 计算行数
|
||
final totalRows = (chapters.length / itemsPerRow).ceil();
|
||
|
||
return Column(
|
||
children: List.generate(totalRows, (rowIndex) {
|
||
final startIndex = rowIndex * itemsPerRow;
|
||
final endIndex = (startIndex + itemsPerRow).clamp(0, chapters.length);
|
||
|
||
return Padding(
|
||
padding: EdgeInsets.only(bottom: rowIndex < totalRows - 1 ? 16.0 : 0),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
for (int i = startIndex; i < endIndex; i++) ...[
|
||
if (i > startIndex) const SizedBox(width: 16.0),
|
||
SizedBox(
|
||
width: 450,
|
||
child: _OptimizedChapterCard(
|
||
actId: act.id,
|
||
chapter: chapters[i],
|
||
novelId: novelId,
|
||
editorBloc: editorBloc,
|
||
),
|
||
),
|
||
],
|
||
const Spacer(),
|
||
],
|
||
),
|
||
);
|
||
}),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 优化的章节卡片 - 保持原有功能但提升性能
|
||
class _OptimizedChapterCard extends StatelessWidget {
|
||
const _OptimizedChapterCard({
|
||
required this.actId,
|
||
required this.chapter,
|
||
required this.novelId,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final String actId;
|
||
final novel_models.Chapter chapter;
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : Colors.grey.shade300,
|
||
style: BorderStyle.solid,
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withOpacity(0.04),
|
||
blurRadius: 4,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 章节标题栏
|
||
_ChapterHeader(
|
||
actId: actId,
|
||
chapter: chapter,
|
||
editorBloc: editorBloc,
|
||
),
|
||
// 场景列表 - 优化版本,限制显示数量
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 场景列表 - 限制最多显示5个场景以提升性能
|
||
...chapter.scenes.take(5).toList().asMap().entries.map((entry) =>
|
||
_OptimizedSceneItem(
|
||
actId: actId,
|
||
chapterId: chapter.id,
|
||
scene: entry.value,
|
||
sceneNumber: entry.key + 1,
|
||
novelId: novelId,
|
||
editorBloc: editorBloc,
|
||
),
|
||
),
|
||
// 如果有更多场景,显示省略提示
|
||
if (chapter.scenes.length > 5) ...[
|
||
Container(
|
||
margin: const EdgeInsets.only(top: 6),
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey100 : WebTheme.grey100,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
'还有 ${chapter.scenes.length - 5} 个场景...',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
fontStyle: FontStyle.italic,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
const SizedBox(height: 8),
|
||
_AddSceneButton(
|
||
actId: actId,
|
||
chapterId: chapter.id,
|
||
editorBloc: editorBloc,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 优化的场景项组件 - 简化版本但保持核心功能
|
||
class _OptimizedSceneItem extends StatefulWidget {
|
||
const _OptimizedSceneItem({
|
||
required this.actId,
|
||
required this.chapterId,
|
||
required this.scene,
|
||
required this.sceneNumber,
|
||
required this.novelId,
|
||
required this.editorBloc,
|
||
});
|
||
|
||
final String actId;
|
||
final String chapterId;
|
||
final novel_models.Scene scene;
|
||
final int sceneNumber;
|
||
final String novelId;
|
||
final editor.EditorBloc editorBloc;
|
||
|
||
@override
|
||
State<_OptimizedSceneItem> createState() => _OptimizedSceneItemState();
|
||
}
|
||
|
||
class _OptimizedSceneItemState extends State<_OptimizedSceneItem> {
|
||
late TextEditingController _summaryController;
|
||
bool _isEditing = true;
|
||
bool _hasUnsavedChanges = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_summaryController = TextEditingController(text: widget.scene.summary.content);
|
||
_summaryController.addListener(_onSummaryChanged);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_summaryController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _onSummaryChanged() {
|
||
final hasChanges = _summaryController.text != widget.scene.summary.content;
|
||
if (hasChanges != _hasUnsavedChanges) {
|
||
setState(() {
|
||
_hasUnsavedChanges = hasChanges;
|
||
});
|
||
}
|
||
}
|
||
|
||
void _saveSummary() {
|
||
if (_hasUnsavedChanges) {
|
||
widget.editorBloc.add(editor.UpdateSummary(
|
||
novelId: widget.novelId,
|
||
actId: widget.actId,
|
||
chapterId: widget.chapterId,
|
||
sceneId: widget.scene.id,
|
||
summary: _summaryController.text,
|
||
));
|
||
setState(() {
|
||
_hasUnsavedChanges = false;
|
||
_isEditing = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
void _navigateToScene() {
|
||
widget.editorBloc.add(editor.NavigateToSceneFromPlan(
|
||
actId: widget.actId,
|
||
chapterId: widget.chapterId,
|
||
sceneId: widget.scene.id,
|
||
));
|
||
|
||
Future.delayed(const Duration(milliseconds: 300), () {
|
||
// 跳转后可在外部触发切换
|
||
});
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final hasContent = widget.scene.summary.content.isNotEmpty;
|
||
final wordCount = widget.scene.content.length;
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.only(bottom: 6),
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.getSurfaceColor(context),
|
||
borderRadius: BorderRadius.circular(6),
|
||
border: Border.all(color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey300 : WebTheme.grey200),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// 工具栏区域 - 简化版
|
||
Container(
|
||
height: 24, // 减少高度
|
||
decoration: BoxDecoration(
|
||
color: WebTheme.isDarkMode(context) ? WebTheme.darkGrey100 : WebTheme.grey50,
|
||
borderRadius: const BorderRadius.only(
|
||
topLeft: Radius.circular(6),
|
||
topRight: Radius.circular(6),
|
||
),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 0),
|
||
child: Row(
|
||
children: [
|
||
// 场景标签
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade200,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
child: Text(
|
||
'S${widget.sceneNumber}',
|
||
style: const TextStyle(
|
||
fontSize: 9,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black54,
|
||
),
|
||
),
|
||
),
|
||
|
||
const SizedBox(width: 6),
|
||
|
||
// 字数统计(如果有)
|
||
if (wordCount > 0) ...[
|
||
Text(
|
||
'${wordCount}w',
|
||
style: TextStyle(
|
||
fontSize: 9,
|
||
color: Colors.blue.shade600,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
const SizedBox(width: 6),
|
||
],
|
||
|
||
const Spacer(),
|
||
|
||
// 保存指示器
|
||
if (_hasUnsavedChanges) ...[
|
||
GestureDetector(
|
||
onTap: _saveSummary,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||
decoration: BoxDecoration(
|
||
color: Colors.green.shade600,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
child: const Text(
|
||
'保存',
|
||
style: TextStyle(
|
||
fontSize: 8,
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
],
|
||
|
||
// 跳转按钮
|
||
_SmallIconButton(
|
||
icon: Icons.launch,
|
||
size: 10,
|
||
onPressed: _navigateToScene,
|
||
),
|
||
|
||
// 编辑切换按钮
|
||
_SmallIconButton(
|
||
icon: _isEditing ? Icons.visibility : Icons.edit,
|
||
size: 10,
|
||
onPressed: () {
|
||
setState(() {
|
||
_isEditing = !_isEditing;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// 摘要内容区域 - 放大版
|
||
Container(
|
||
width: double.infinity,
|
||
constraints: const BoxConstraints(
|
||
minHeight: 200, // 再放大
|
||
),
|
||
padding: const EdgeInsets.all(8),
|
||
child: _isEditing
|
||
? TextField(
|
||
controller: _summaryController,
|
||
decoration: InputDecoration(
|
||
hintText: '输入场景摘要...',
|
||
border: InputBorder.none,
|
||
hintStyle: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
),
|
||
),
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getTextColor(context),
|
||
height: 1.8,
|
||
),
|
||
maxLines: null,
|
||
minLines: 5,
|
||
onSubmitted: (_) => _saveSummary(),
|
||
)
|
||
: GestureDetector(
|
||
onTap: () {
|
||
setState(() {
|
||
_isEditing = true;
|
||
});
|
||
},
|
||
child: Container(
|
||
width: double.infinity,
|
||
child: hasContent
|
||
? Text(
|
||
widget.scene.summary.content,
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getTextColor(context),
|
||
height: 1.8,
|
||
),
|
||
// 自适应高度,不再省略
|
||
)
|
||
: Text(
|
||
'点击添加场景描述...',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
color: WebTheme.getSecondaryTextColor(context),
|
||
fontStyle: FontStyle.italic,
|
||
height: 1.8,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Plan项目类型枚举
|
||
enum _PlanItemType {
|
||
actHeader,
|
||
chapterBatch,
|
||
addChapter,
|
||
addAct,
|
||
}
|
||
|
||
/// Plan项目数据类
|
||
class _PlanItem {
|
||
const _PlanItem({
|
||
required this.type,
|
||
this.act,
|
||
this.chapters,
|
||
this.actIndex,
|
||
this.batchStart,
|
||
});
|
||
|
||
final _PlanItemType type;
|
||
final novel_models.Act? act;
|
||
final List<novel_models.Chapter>? chapters;
|
||
final int? actIndex;
|
||
final int? batchStart;
|
||
} |