Files
MaliangAINovalWriter/AINoval/lib/screens/editor/widgets/setting_preview_card.dart
2025-09-10 00:07:52 +08:00

561 lines
17 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ainoval/models/novel_setting_item.dart';
import 'package:ainoval/models/setting_type.dart';
import 'package:ainoval/models/setting_group.dart';
import 'package:ainoval/blocs/setting/setting_bloc.dart';
import 'package:ainoval/utils/web_theme.dart';
import 'package:ainoval/utils/logger.dart';
import 'package:ainoval/screens/editor/widgets/novel_setting_detail.dart';
/// 设定信息预览卡片组件
/// 显示设定的基本信息(分类、名称、设定组、图片、描述)
class SettingPreviewCard extends StatefulWidget {
final String settingId;
final String novelId;
final Offset position;
final VoidCallback? onClose;
const SettingPreviewCard({
Key? key,
required this.settingId,
required this.novelId,
required this.position,
this.onClose,
}) : super(key: key);
@override
State<SettingPreviewCard> createState() => _SettingPreviewCardState();
}
class _SettingPreviewCardState extends State<SettingPreviewCard> with TickerProviderStateMixin {
static const String _tag = 'SettingPreviewCard';
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
late Animation<double> _opacityAnimation;
NovelSettingItem? _settingItem;
SettingGroup? _settingGroup;
bool _isLoading = true;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOutCubic,
));
_opacityAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
));
_loadSettingData();
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
/// 加载设定数据
void _loadSettingData() {
try {
final settingBloc = context.read<SettingBloc>();
final state = settingBloc.state;
// 查找设定条目
_settingItem = state.items.firstWhere(
(item) => item.id == widget.settingId,
orElse: () => NovelSettingItem(name: ''),
);
if (_settingItem != null) {
// 查找设定组
_settingGroup = state.groups.firstWhere(
(group) => group.itemIds?.any((item) => item == widget.settingId) == true,
orElse: () => SettingGroup(name: ''),
);
}
setState(() {
_isLoading = false;
});
AppLogger.d(_tag, '设定数据加载完成: ${_settingItem?.name ?? "未找到"}');
} catch (e) {
AppLogger.e(_tag, '加载设定数据失败', e);
setState(() {
_isLoading = false;
});
}
}
/// 获取设定类型图标
IconData _getTypeIcon() {
if (_settingItem?.type == null) return Icons.article;
final settingType = SettingType.fromValue(_settingItem!.type!);
switch (settingType) {
case SettingType.character:
return Icons.person;
case SettingType.location:
return Icons.place;
case SettingType.item:
return Icons.inventory_2;
case SettingType.lore:
return Icons.public;
case SettingType.event:
return Icons.event;
case SettingType.concept:
return Icons.auto_awesome;
case SettingType.faction:
return Icons.groups;
case SettingType.creature:
return Icons.pets;
case SettingType.magicSystem:
return Icons.auto_fix_high;
case SettingType.technology:
return Icons.science;
case SettingType.culture:
return Icons.emoji_people;
case SettingType.history:
return Icons.history;
case SettingType.organization:
return Icons.apartment;
case SettingType.worldview:
return Icons.public;
case SettingType.pleasurePoint:
return Icons.whatshot;
case SettingType.anticipationHook:
return Icons.bolt;
case SettingType.theme:
return Icons.category;
case SettingType.tone:
return Icons.tonality;
case SettingType.style:
return Icons.brush;
case SettingType.trope:
return Icons.theater_comedy;
case SettingType.plotDevice:
return Icons.schema;
case SettingType.powerSystem:
return Icons.flash_on;
case SettingType.timeline:
return Icons.timeline;
case SettingType.religion:
return Icons.account_balance;
case SettingType.politics:
return Icons.gavel;
case SettingType.economy:
return Icons.attach_money;
case SettingType.geography:
return Icons.map;
default:
return Icons.article;
}
}
/// 获取设定类型显示名称
String _getTypeDisplayName() {
if (_settingItem?.type == null) return '其他';
return SettingType.fromValue(_settingItem!.type!).displayName;
}
/// 处理标题点击
void _handleTitleTap() {
AppLogger.d(_tag, '点击设定标题,打开详情卡片: ${_settingItem?.name}');
// 关闭当前预览卡片
_close();
// 延迟一小段时间后打开详情卡片,确保预览卡片完全关闭
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted && _settingItem != null) {
FloatingNovelSettingDetail.show(
context: context,
itemId: _settingItem!.id,
novelId: widget.novelId,
groupId: _settingGroup?.id,
isEditing: false,
onSave: (item, groupId) {
// 保存成功后可以做一些处理
AppLogger.i(_tag, '设定详情保存成功: ${item.name}');
},
onCancel: () {
// 取消操作
AppLogger.d(_tag, '设定详情编辑取消');
},
);
}
});
}
/// 关闭卡片
void _close() {
_animationController.reverse().then((_) {
widget.onClose?.call();
});
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
final isDark = WebTheme.isDarkMode(context);
// 计算卡片位置,确保不超出屏幕边界
const cardWidth = 320.0;
const cardHeight = 200.0;
double left = widget.position.dx;
double top = widget.position.dy;
// 调整水平位置
if (left + cardWidth > screenSize.width) {
left = screenSize.width - cardWidth - 16;
}
if (left < 16) {
left = 16;
}
// 调整垂直位置
if (top + cardHeight > screenSize.height) {
top = widget.position.dy - cardHeight - 10; // 显示在鼠标上方
}
if (top < 16) {
top = 16;
}
return Positioned(
left: left,
top: top,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _scaleAnimation.value,
child: Opacity(
opacity: _opacityAnimation.value,
child: Material(
elevation: 12,
borderRadius: BorderRadius.circular(12),
color: Colors.transparent,
shadowColor: Theme.of(context).colorScheme.shadow.withOpacity(0.3),
child: Container(
width: cardWidth,
constraints: const BoxConstraints(
maxHeight: cardHeight,
),
decoration: BoxDecoration(
color: WebTheme.getSurfaceColor(context),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isDark ? WebTheme.darkGrey700 : WebTheme.grey300,
width: 1.5,
),
),
child: _buildCardContent(isDark),
),
),
),
);
},
),
);
}
/// 构建卡片内容
Widget _buildCardContent(bool isDark) {
if (_isLoading) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: CircularProgressIndicator(),
),
);
}
if (_settingItem == null) {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
size: 32,
color: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(height: 8),
Text(
'设定不存在',
style: TextStyle(
fontSize: 14,
color: WebTheme.getSecondaryTextColor(context),
),
),
],
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 头部区域
_buildHeader(isDark),
// 分隔线
Container(
height: 1,
color: isDark ? WebTheme.darkGrey800 : WebTheme.grey200,
),
// 内容区域
Flexible(
child: _buildContent(isDark),
),
],
);
}
/// 构建头部区域
Widget _buildHeader(bool isDark) {
return Container(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// 设定图片或类型图标
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isDark ? WebTheme.darkGrey800 : WebTheme.grey100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark ? WebTheme.darkGrey700 : WebTheme.grey300,
width: 1,
),
),
child: _settingItem!.imageUrl != null && _settingItem!.imageUrl!.isNotEmpty
? ClipRRect(
borderRadius: BorderRadius.circular(7),
child: Image.network(
_settingItem!.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Icon(
_getTypeIcon(),
size: 24,
color: WebTheme.getTextColor(context),
);
},
),
)
: Icon(
_getTypeIcon(),
size: 24,
color: WebTheme.getTextColor(context),
),
),
const SizedBox(width: 12),
// 设定信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 设定名称(可点击)
GestureDetector(
onTap: _handleTitleTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Text(
_settingItem!.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
decoration: TextDecoration.underline,
decorationColor: WebTheme.getTextColor(context).withOpacity(0.3),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
const SizedBox(height: 4),
// 类型和设定组
Row(
children: [
// 设定类型
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: WebTheme.getTextColor(context).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_getTypeDisplayName(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: WebTheme.getTextColor(context),
),
),
),
if (_settingGroup != null) ...[
const SizedBox(width: 8),
// 设定组
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: WebTheme.getSecondaryTextColor(context).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_settingGroup!.name,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: WebTheme.getSecondaryTextColor(context),
),
),
),
],
],
),
],
),
),
// 关闭按钮
GestureDetector(
onTap: _close,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(4),
),
child: Icon(
Icons.close,
size: 16,
color: WebTheme.getSecondaryTextColor(context),
),
),
),
),
],
),
);
}
/// 构建内容区域
Widget _buildContent(bool isDark) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 描述内容
if (_settingItem!.description != null && _settingItem!.description!.isNotEmpty) ...[
Text(
'描述',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
),
),
const SizedBox(height: 6),
Flexible(
child: Text(
_settingItem!.description!,
style: TextStyle(
fontSize: 13,
height: 1.4,
color: WebTheme.getSecondaryTextColor(context),
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
),
] else if (_settingItem!.content != null && _settingItem!.content!.isNotEmpty) ...[
Text(
'内容',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: WebTheme.getTextColor(context),
),
),
const SizedBox(height: 6),
Flexible(
child: Text(
_settingItem!.content!,
style: TextStyle(
fontSize: 13,
height: 1.4,
color: WebTheme.getSecondaryTextColor(context),
),
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
),
] else ...[
Center(
child: Text(
'暂无描述',
style: TextStyle(
fontSize: 13,
color: WebTheme.getSecondaryTextColor(context).withOpacity(0.6),
fontStyle: FontStyle.italic,
),
),
),
],
const SizedBox(height: 8),
// 提示文本
Text(
'点击标题查看详情',
style: TextStyle(
fontSize: 11,
color: WebTheme.getSecondaryTextColor(context).withOpacity(0.7),
fontStyle: FontStyle.italic,
),
),
],
),
);
}
}