// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_dimens.dart'; import '../../core/widgets/cover_play_count.dart'; import '../../core/widgets/remote_image.dart'; const double _selectMarkSize = 22; const double _selectMarkInset = 6; const double _selectMarkIconSize = 16; const double _selectBorderWidth = 1.5; class FollowingGridCard extends StatelessWidget { const FollowingGridCard({ super.key, required this.dramaId, required this.title, required this.posterUrl, required this.viewCount, required this.onTap, this.progress, this.editing = false, this.selected, this.onRemove, this.updateBadge, }); final String dramaId; final String title; final String posterUrl; final int viewCount; final VoidCallback onTap; final double? progress; final bool editing; final bool? selected; final VoidCallback? onRemove; /// 左上角「有更新」角标文案;null 不显示。 /// 待后端在 Drama 增加更新状态字段后由收藏网格传入。 final String? updateBadge; @override Widget build(BuildContext context) { return InkWell( key: Key('following-card-$dramaId'), borderRadius: BorderRadius.circular(AppDimens.homeCardRadius), onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( key: Key('following-cover-$dramaId'), aspectRatio: AppDimens.cardCoverAspectRatio, child: Stack( clipBehavior: Clip.hardEdge, children: [ Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(AppDimens.coverRadius), child: RemoteImage( posterUrl, fit: BoxFit.cover, fallbackColor: AppColors.contentSurface, ), ), ), if (selected != null) Positioned( top: _selectMarkInset, right: _selectMarkInset, child: _SelectMark( key: Key('following-select-$dramaId'), selected: selected!, ), ) else if (editing) Positioned( top: -2, right: -2, child: GestureDetector( key: Key('following-remove-$dramaId'), onTap: onRemove, child: const CircleAvatar( radius: 12, backgroundColor: AppColors.contentChip, child: Icon( Icons.remove_rounded, color: AppColors.textPrimary, size: 17, ), ), ), ), const Positioned( left: 0, right: 0, bottom: 0, child: CoverBottomScrim(), ), Positioned( right: AppDimens.cardPlayCountRight, bottom: progress == null ? AppDimens.cardPlayCountBottom : AppDimens.followingProgressHeight + 4, child: CoverPlayCount( key: Key('following-views-$dramaId'), count: viewCount, ), ), if (updateBadge != null && selected == null) Positioned( left: AppDimens.followingBadgeInset, top: AppDimens.followingBadgeInset, child: Container( padding: const EdgeInsets.symmetric( horizontal: AppDimens.followingBadgePaddingH, vertical: AppDimens.followingBadgePaddingV, ), decoration: BoxDecoration( color: AppColors.brandRed, borderRadius: BorderRadius.circular( AppDimens.followingBadgeRadius, ), ), child: Text( updateBadge!, style: const TextStyle( color: AppColors.onPrimary, fontSize: AppDimens.followingBadgeFont, fontWeight: FontWeight.w400, height: 1, ), ), ), ), if (progress != null) Positioned( left: 0, right: 0, bottom: 0, child: LinearProgressIndicator( key: Key('following-progress-$dramaId'), value: progress!.clamp(0.0, 1.0).toDouble(), minHeight: AppDimens.followingProgressHeight, backgroundColor: AppColors.inactiveTrack, valueColor: const AlwaysStoppedAnimation( AppColors.brandRed, ), ), ), ], ), ), const SizedBox(height: AppDimens.cardCoverTitleGap), Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, textAlign: TextAlign.left, style: const TextStyle( color: AppColors.textPrimary, fontSize: AppDimens.followingCardTitleFontNew, fontWeight: FontWeight.w400, height: AppDimens.cardTitleLineHeight, ), ), ], ), ); } } class _SelectMark extends StatelessWidget { const _SelectMark({super.key, required this.selected}); final bool selected; @override Widget build(BuildContext context) { return Container( width: _selectMarkSize, height: _selectMarkSize, decoration: BoxDecoration( shape: BoxShape.circle, color: selected ? AppColors.brandRed : AppColors.transparent, border: Border.all( color: selected ? AppColors.brandRed : AppColors.textPrimary, width: _selectBorderWidth, ), ), child: selected ? const Icon( Icons.check_rounded, size: _selectMarkIconSize, color: AppColors.onPrimary, ) : null, ); } } class FollowingAddCard extends StatelessWidget { const FollowingAddCard({super.key, required this.onTap}); final VoidCallback onTap; @override Widget build(BuildContext context) { return InkWell( key: const Key('following-add-card'), borderRadius: BorderRadius.circular(AppDimens.homeCardRadius), onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AspectRatio( aspectRatio: AppDimens.cardCoverAspectRatio, child: DecoratedBox( decoration: BoxDecoration( color: AppColors.contentSurface, borderRadius: BorderRadius.circular(AppDimens.homeCardRadius), ), child: const Center( child: CustomPaint( key: Key('following-add-icon'), size: Size.square(AppDimens.followingAddIconSize), painter: _FollowingPlusPainter(), ), ), ), ), ], ), ); } } class _FollowingPlusPainter extends CustomPainter { const _FollowingPlusPainter(); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = AppColors.textSecondary ..strokeWidth = AppDimens.followingAddIconStroke ..strokeCap = StrokeCap.round; final center = size.center(Offset.zero); canvas ..drawLine(Offset(center.dx, 0), Offset(center.dx, size.height), paint) ..drawLine(Offset(0, center.dy), Offset(size.width, center.dy), paint); } @override bool shouldRepaint(covariant _FollowingPlusPainter oldDelegate) => false; }