马良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,486 @@
import 'package:flutter/material.dart';
import 'package:ainoval/utils/web_theme.dart';
import 'package:ainoval/models/novel_summary.dart';
import 'package:ainoval/services/image_cache_service.dart';
class CompactNovelCard extends StatefulWidget {
final NovelSummary novel;
final VoidCallback? onContinueWriting;
final VoidCallback? onEdit;
final VoidCallback? onShare;
final VoidCallback? onDelete;
const CompactNovelCard({
Key? key,
required this.novel,
this.onContinueWriting,
this.onEdit,
this.onShare,
this.onDelete,
}) : super(key: key);
@override
State<CompactNovelCard> createState() => _CompactNovelCardState();
}
class _CompactNovelCardState extends State<CompactNovelCard> with TickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
bool _isHovered = false;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
));
_scaleAnimation = Tween<double>(
begin: 0.95,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
));
_animationController.forward();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
String _getNovelStatus() {
if (widget.novel.wordCount < 1000) {
return '草稿';
} else if (widget.novel.completionPercentage >= 100.0) {
return '已完结';
} else {
return '连载中';
}
}
Color _getStatusColor(String status, BuildContext context) {
final isDark = WebTheme.isDarkMode(context);
switch (status) {
case '草稿':
return isDark ? WebTheme.darkGrey400 : WebTheme.grey400;
case '连载中':
return Theme.of(context).colorScheme.primary;
case '已完结':
return Theme.of(context).colorScheme.secondary;
default:
return isDark ? WebTheme.darkGrey400 : WebTheme.grey400;
}
}
Color _getStatusBackgroundColor(String status, BuildContext context) {
final isDark = WebTheme.isDarkMode(context);
switch (status) {
case '草稿':
return isDark ? WebTheme.darkGrey200 : WebTheme.grey200;
case '连载中':
return Theme.of(context).colorScheme.primaryContainer.withOpacity(isDark ? 0.2 : 1.0);
case '已完结':
return Theme.of(context).colorScheme.secondaryContainer.withOpacity(isDark ? 0.2 : 1.0);
default:
return isDark ? WebTheme.darkGrey200 : WebTheme.grey200;
}
}
String _getCoverImageUrl() {
if (widget.novel.coverUrl.isNotEmpty) {
return widget.novel.coverUrl;
}
// Use Picsum Photos as fallback with unique ID based on novel ID
final randomId = widget.novel.id.hashCode.abs() % 1000;
return 'https://picsum.photos/400/300?random=$randomId';
}
String _formatLastEditTime() {
final now = DateTime.now();
final diff = now.difference(widget.novel.lastEditTime);
if (diff.inDays > 30) {
return '${(diff.inDays / 30).floor()}个月前';
} else if (diff.inDays > 0) {
return '${diff.inDays}天前';
} else if (diff.inHours > 0) {
return '${diff.inHours}小时前';
} else if (diff.inMinutes > 0) {
return '${diff.inMinutes}分钟前';
} else {
return '刚刚';
}
}
@override
Widget build(BuildContext context) {
final isDark = WebTheme.isDarkMode(context);
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
transform: Matrix4.identity()
..scale(_isHovered ? 1.02 : 1.0),
decoration: BoxDecoration(
color: WebTheme.getCardColor(context),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _isHovered
? (isDark ? WebTheme.darkGrey500 : WebTheme.grey500)
: WebTheme.getBorderColor(context),
width: 1,
),
boxShadow: _isHovered ? [
BoxShadow(
color: WebTheme.getShadowColor(context, opacity: 0.15),
blurRadius: 20,
offset: const Offset(0, 4),
),
] : [
BoxShadow(
color: WebTheme.getShadowColor(context, opacity: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Cover Image Area - 更紧凑的比例
Expanded(
flex: 3,
child: AspectRatio(
aspectRatio: 3 / 2,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
(isDark ? WebTheme.darkGrey300 : WebTheme.grey300).withOpacity(0.2),
(isDark ? WebTheme.darkGrey200 : WebTheme.grey200).withOpacity(0.1),
],
),
),
child: ImageCacheService().getAdaptiveImage(
imageUrl: _getCoverImageUrl(),
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
backgroundColor: WebTheme.getCardColor(context),
borderRadius: BorderRadius.circular(12),
placeholder: 'menu_book',
),
),
// Status Badge
Positioned(
top: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: _getStatusBackgroundColor(_getNovelStatus(), context),
borderRadius: BorderRadius.circular(8),
),
child: Text(
_getNovelStatus(),
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: _getStatusColor(_getNovelStatus(), context),
),
),
),
),
// More Options Button
Positioned(
top: 6,
right: 6,
child: Material(
color: (isDark ? WebTheme.darkGrey100 : WebTheme.white).withOpacity(0.9),
borderRadius: BorderRadius.circular(6),
child: PopupMenuButton<String>(
icon: Icon(
Icons.more_horiz,
size: 14,
color: WebTheme.getTextColor(context, isPrimary: false),
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 24, minHeight: 24),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
itemBuilder: (context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 14, color: WebTheme.getTextColor(context, isPrimary: false)),
const SizedBox(width: 6),
const Text('编辑', style: TextStyle(fontSize: 12)),
],
),
),
PopupMenuItem(
value: 'share',
child: Row(
children: [
Icon(Icons.share, size: 14, color: WebTheme.getTextColor(context, isPrimary: false)),
const SizedBox(width: 6),
const Text('分享', style: TextStyle(fontSize: 12)),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 14, color: WebTheme.error),
const SizedBox(width: 6),
Text('删除', style: TextStyle(color: WebTheme.error, fontSize: 12)),
],
),
),
],
onSelected: (value) {
switch (value) {
case 'edit':
widget.onEdit?.call();
break;
case 'share':
widget.onShare?.call();
break;
case 'delete':
widget.onDelete?.call();
break;
}
},
),
),
),
],
),
),
),
// Content Area - 更紧凑的布局
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 6, 8, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Title
Text(
widget.novel.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: _isHovered
? WebTheme.getPrimaryColor(context)
: WebTheme.getTextColor(context),
),
),
const SizedBox(height: 2),
// Description
Expanded(
child: Text(
widget.novel.description.isNotEmpty
? widget.novel.description
: '暂无描述',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: WebTheme.getSecondaryTextColor(context),
height: 1.2,
),
),
),
const SizedBox(height: 4),
// Category and Rating
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
border: Border.all(
color: WebTheme.getBorderColor(context),
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.novel.seriesName.isNotEmpty
? widget.novel.seriesName
: '独立作品',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: WebTheme.getTextColor(context, isPrimary: false),
),
),
),
if (widget.novel.completionPercentage > 0) ...[
const SizedBox(width: 6),
Row(
children: [
Icon(
Icons.percent,
size: 12,
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 2),
Text(
'${widget.novel.completionPercentage.toStringAsFixed(0)}%',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: WebTheme.getSecondaryTextColor(context),
),
),
],
),
],
],
),
const SizedBox(height: 4),
// Stats - 单行显示
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.menu_book,
size: 9,
color: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(width: 1),
Text(
'${(widget.novel.wordCount / 1000).toStringAsFixed(0)}k字',
style: TextStyle(
fontSize: 9,
color: WebTheme.getSecondaryTextColor(context),
),
),
const SizedBox(width: 6),
Icon(
Icons.schedule,
size: 9,
color: WebTheme.getSecondaryTextColor(context),
),
const SizedBox(width: 1),
Text(
'${widget.novel.readTime}分钟',
style: TextStyle(
fontSize: 9,
color: WebTheme.getSecondaryTextColor(context),
),
),
],
),
Text(
_formatLastEditTime(),
style: TextStyle(
fontSize: 9,
color: WebTheme.getSecondaryTextColor(context),
),
),
],
),
const SizedBox(height: 6),
// Continue Writing Button
SizedBox(
width: double.infinity,
height: 24,
child: OutlinedButton(
onPressed: widget.onContinueWriting,
style: OutlinedButton.styleFrom(
foregroundColor: _isHovered
? WebTheme.white
: WebTheme.getTextColor(context),
backgroundColor: _isHovered
? WebTheme.getPrimaryColor(context)
: Colors.transparent,
side: BorderSide(
color: WebTheme.getBorderColor(context),
width: 1,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.symmetric(horizontal: 12),
),
child: const Text(
'继续创作',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
),
],
),
),
),
),
),
);
}
Widget _buildPlaceholder(BuildContext context) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
WebTheme.getPrimaryColor(context).withOpacity(0.1),
WebTheme.getSecondaryColor(context).withOpacity(0.05),
],
),
),
child: Center(
child: Icon(
Icons.menu_book,
size: 32,
color: WebTheme.getSecondaryTextColor(context).withOpacity(0.5),
),
),
);
}
}