428 lines
13 KiB
Dart
428 lines
13 KiB
Dart
import 'package:ainoval/blocs/editor_version_bloc.dart';
|
|
import 'package:ainoval/config/app_config.dart';
|
|
import 'package:ainoval/models/scene_version.dart';
|
|
import 'package:ainoval/ui/common/loading_indicator.dart';
|
|
import 'package:ainoval/ui/common/no_data_placeholder.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:ainoval/utils/web_theme.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
/// 场景历史对话框
|
|
class SceneHistoryDialog extends StatefulWidget {
|
|
|
|
const SceneHistoryDialog({
|
|
Key? key,
|
|
required this.novelId,
|
|
required this.chapterId,
|
|
required this.sceneId,
|
|
}) : super(key: key);
|
|
final String novelId;
|
|
final String chapterId;
|
|
final String sceneId;
|
|
|
|
@override
|
|
State<SceneHistoryDialog> createState() => _SceneHistoryDialogState();
|
|
}
|
|
|
|
class _SceneHistoryDialogState extends State<SceneHistoryDialog> {
|
|
int? _selectedIndex;
|
|
int? _compareIndex;
|
|
bool _isComparing = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// 加载历史记录
|
|
context.read<EditorVersionBloc>().add(EditorVersionFetchHistory(
|
|
novelId: widget.novelId,
|
|
chapterId: widget.chapterId,
|
|
sceneId: widget.sceneId,
|
|
));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
elevation: 0,
|
|
backgroundColor: Colors.transparent,
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width * 0.8,
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).dialogBackgroundColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
_buildHeader(),
|
|
Expanded(
|
|
child: _buildContent(),
|
|
),
|
|
_buildFooter(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建对话框头部
|
|
Widget _buildHeader() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'场景历史版本',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 构建对话框内容
|
|
Widget _buildContent() {
|
|
return BlocConsumer<EditorVersionBloc, EditorVersionState>(
|
|
listener: (context, state) {
|
|
if (state is EditorVersionDiffLoaded) {
|
|
// 显示差异对话框
|
|
_showDiffDialog(context, state.diff);
|
|
} else if (state is EditorVersionRestored) {
|
|
// 关闭对话框并返回恢复的场景
|
|
Navigator.of(context).pop(state.scene);
|
|
} else if (state is EditorVersionError) {
|
|
// 显示错误信息
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(state.message)),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
if (state is EditorVersionLoading) {
|
|
return const Center(child: LoadingIndicator());
|
|
} else if (state is EditorVersionHistoryEmpty) {
|
|
return const NoDataPlaceholder(
|
|
message: '暂无历史版本',
|
|
icon: Icons.history,
|
|
);
|
|
} else if (state is EditorVersionHistoryLoaded) {
|
|
return _buildHistoryList(state.history);
|
|
} else if (state is EditorVersionError) {
|
|
return Center(child: Text(state.message));
|
|
}
|
|
|
|
return const SizedBox.shrink();
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 构建历史版本列表
|
|
Widget _buildHistoryList(List<SceneHistoryEntry> history) {
|
|
final dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss');
|
|
|
|
return ListView.builder(
|
|
itemCount: history.length,
|
|
itemBuilder: (context, index) {
|
|
final entry = history[index];
|
|
final isSelected = _selectedIndex == index;
|
|
final isComparing = _compareIndex == index;
|
|
|
|
return Card(
|
|
elevation: isSelected ? 4 : 1,
|
|
color: isSelected
|
|
? WebTheme.getPrimaryColor(context).withOpacity(0.1)
|
|
: (isComparing ? WebTheme.warning.withOpacity(0.1) : null),
|
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
child: ListTile(
|
|
title: Row(
|
|
children: [
|
|
Text('版本 ${index + 1}'),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
dateFormat.format(entry.updatedAt),
|
|
style: Theme.of(context).textTheme.bodySmall,
|
|
),
|
|
],
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('修改人: ${entry.updatedBy}'),
|
|
Text('原因: ${entry.reason}'),
|
|
],
|
|
),
|
|
trailing: _isComparing
|
|
? IconButton(
|
|
icon: Icon(
|
|
isComparing ? Icons.check_circle : Icons.circle_outlined,
|
|
color: isComparing ? Colors.amber : null,
|
|
),
|
|
onPressed: () {
|
|
setState(() {
|
|
if (isComparing) {
|
|
_compareIndex = null;
|
|
} else {
|
|
_compareIndex = index;
|
|
}
|
|
});
|
|
},
|
|
)
|
|
: null,
|
|
onTap: () {
|
|
setState(() {
|
|
if (_isComparing) {
|
|
// 比较模式下,点击切换选择状态
|
|
if (_compareIndex == null || _compareIndex == index) {
|
|
_compareIndex = index;
|
|
} else {
|
|
// 已有两个不同的版本,触发比较
|
|
_triggerCompare(_compareIndex!, index);
|
|
}
|
|
} else {
|
|
// 普通模式下,切换选中状态
|
|
if (_selectedIndex == index) {
|
|
_selectedIndex = null;
|
|
} else {
|
|
_selectedIndex = index;
|
|
}
|
|
}
|
|
});
|
|
},
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 构建对话框底部
|
|
Widget _buildFooter() {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () {
|
|
setState(() {
|
|
_isComparing = !_isComparing;
|
|
if (!_isComparing) {
|
|
_compareIndex = null;
|
|
}
|
|
});
|
|
},
|
|
child: Text(_isComparing ? '取消比较' : '比较版本'),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: _selectedIndex != null ? _restoreVersion : null,
|
|
child: const Text('恢复此版本'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 触发版本比较
|
|
void _triggerCompare(int index1, int index2) {
|
|
// 确保小索引在前
|
|
final versionIndex1 = index1 < index2 ? index1 : index2;
|
|
final versionIndex2 = index1 < index2 ? index2 : index1;
|
|
|
|
context.read<EditorVersionBloc>().add(EditorVersionCompare(
|
|
novelId: widget.novelId,
|
|
chapterId: widget.chapterId,
|
|
sceneId: widget.sceneId,
|
|
versionIndex1: versionIndex1,
|
|
versionIndex2: versionIndex2,
|
|
));
|
|
}
|
|
|
|
/// 恢复到所选版本
|
|
void _restoreVersion() {
|
|
final index = _selectedIndex!;
|
|
|
|
// 显示确认对话框
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('恢复版本'),
|
|
content: const Text('确定要恢复到这个历史版本吗?当前版本将被保存到历史记录中。'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('取消'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
|
|
// 触发恢复事件
|
|
context.read<EditorVersionBloc>().add(EditorVersionRestore(
|
|
novelId: widget.novelId,
|
|
chapterId: widget.chapterId,
|
|
sceneId: widget.sceneId,
|
|
historyIndex: index,
|
|
userId: AppConfig.userId ?? 'system',
|
|
reason: '手动恢复到历史版本',
|
|
));
|
|
},
|
|
child: const Text('确定'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 显示差异对话框
|
|
void _showDiffDialog(BuildContext context, SceneVersionDiff diff) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => Dialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
elevation: 0,
|
|
backgroundColor: Colors.transparent,
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width * 0.8,
|
|
height: MediaQuery.of(context).size.height * 0.7,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).dialogBackgroundColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'版本差异',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: DefaultTabController(
|
|
length: 2,
|
|
child: Column(
|
|
children: [
|
|
const TabBar(
|
|
tabs: [
|
|
Tab(text: '并排对比'),
|
|
Tab(text: '差异格式'),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: TabBarView(
|
|
children: [
|
|
_buildSideBySideView(diff),
|
|
_buildDiffView(diff),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建并排对比视图
|
|
Widget _buildSideBySideView(SceneVersionDiff diff) {
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text('原始版本'),
|
|
),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(diff.originalContent),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const VerticalDivider(),
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
const Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text('新版本'),
|
|
),
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(diff.newContent),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// 构建差异视图
|
|
Widget _buildDiffView(SceneVersionDiff diff) {
|
|
final lines = diff.diff.split('\n');
|
|
|
|
return SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: lines.map((line) {
|
|
Color? color;
|
|
if (line.startsWith('+')) {
|
|
color = Colors.green.shade100;
|
|
} else if (line.startsWith('-')) {
|
|
color = Colors.red.shade100;
|
|
} else if (line.startsWith('@')) {
|
|
color = Colors.blue.shade100;
|
|
}
|
|
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
|
|
color: color,
|
|
child: Text(
|
|
line,
|
|
style: TextStyle(
|
|
fontFamily: 'monospace',
|
|
color: line.startsWith('+')
|
|
? Colors.green.shade900
|
|
: (line.startsWith('-')
|
|
? Colors.red.shade900
|
|
: null),
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
} |