// Author: fruit.com.cn import 'package:flutter/material.dart'; import '../../../core/data/models/drama.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; /// 设计稿(首页 1)推荐页内容区规格: /// 海报卡 110×157、页边距 14、卡距 9、圆角 12。 const double _pageSide = 14; const double _cardWidth = 110; const double _cardCoverHeight = 157; const double _cardGap = 9; const double _cardRadius = AppDimens.coverRadius; /// 海报卡封面以下的高度:上间距 8 + 两行剧名 + 2 + 集数行 + 余量。 /// /// 横向列表和网格都要给一个有限的卡片高度,这里是唯一来源, /// 两行剧名撑高时只改这一处,不会出现某个入口高度不够而溢出。 const double homePosterCardBodyExtent = 62; /// 上新横滑卡板块。无板块标题,卡片左上角带「上新」角标。 /// 内层横向 ListView 会自行识别横向手势,不与外层纵向滚动冲突。 class HomeNewArrivalRow extends StatelessWidget { const HomeNewArrivalRow({ super.key, required this.dramas, required this.onOpen, }); final List dramas; final ValueChanged onOpen; @override Widget build(BuildContext context) { if (dramas.isEmpty) return const SizedBox.shrink(); return SizedBox( key: const Key('home-new-arrival-row'), height: _cardCoverHeight + homePosterCardBodyExtent, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: _pageSide), itemCount: dramas.length, separatorBuilder: (_, _) => const SizedBox(width: _cardGap), itemBuilder: (context, index) => SizedBox( width: _cardWidth, child: HomePosterCard( key: Key('home-new-arrival-${dramas[index].id}'), drama: dramas[index], badge: AppLocalizations.of(context)!.newArrivalBadge, onTap: () => onOpen(dramas[index]), ), ), ), ); } } /// 110×157 海报 + 卡下剧名/集数。 /// /// 封面左上角标签优先用接口返回的短剧标签(`drama.tag` + `drama.tagColor`); /// 接口没有标签时,若传了 [badge](如「上新」)则退回显示该角标。 class HomePosterCard extends StatelessWidget { const HomePosterCard({ super.key, required this.drama, required this.onTap, this.badge, }); final Drama drama; final VoidCallback onTap; final String? badge; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final lang = Localizations.localeOf(context).languageCode; final tag = drama.localizedTag(lang); final tagLabel = tag.isNotEmpty ? tag : (badge ?? ''); return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(_cardRadius), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ClipRRect( borderRadius: BorderRadius.circular(_cardRadius), child: AspectRatio( aspectRatio: _cardWidth / _cardCoverHeight, child: Stack( fit: StackFit.expand, children: [ RemoteImage( drama.posterUrl, fit: BoxFit.cover, fallbackColor: AppColors.contentSurface, ), if (tagLabel.isNotEmpty) Positioned( left: 8, top: 8, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: tag.isNotEmpty ? drama.tagRibbonColor(AppColors.brandRed) : AppColors.brandRed, borderRadius: BorderRadius.circular(6), ), child: Text( tagLabel, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textPrimary, fontSize: 11, fontWeight: FontWeight.w400, height: 1.1, ), ), ), ), ], ), ), ), const SizedBox(height: 8), Text( drama.localizedTitle(lang), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textPrimary, fontSize: 14, fontWeight: FontWeight.w500, height: 1.2, ), ), const SizedBox(height: 2), Text( l10n.totalEpisodesShort(drama.episodeCount), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textTertiary, fontSize: 12, height: 1.2, ), ), ], ), ); } } /// 编辑精选横幅:满宽背景图 + 左上小标签、大标题、副标题。 class HomeEditorPickBanner extends StatelessWidget { const HomeEditorPickBanner({ super.key, required this.drama, required this.onOpen, }); final Drama drama; final ValueChanged onOpen; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final lang = Localizations.localeOf(context).languageCode; return Padding( key: const Key('home-editor-pick'), padding: const EdgeInsets.symmetric(horizontal: _pageSide), child: SizedBox( height: 72, child: ClipRRect( borderRadius: BorderRadius.circular(_cardRadius), child: Stack( fit: StackFit.expand, children: [ RemoteImage( drama.posterUrl, fit: BoxFit.cover, fallbackColor: AppColors.contentSurface, ), const DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ Color(0xE6000000), Color(0x4D000000), Color(0x1A000000), ], ), ), ), Positioned.fill( child: Material( color: AppColors.transparent, child: InkWell(onTap: () => onOpen(drama)), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 14), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.editorPick, maxLines: 1, style: TextStyle( color: AppColors.textPrimary.withValues(alpha: 0.85), fontSize: 11, fontWeight: FontWeight.w500, letterSpacing: 2, ), ), const SizedBox(height: 4), Text( drama.localizedTitle(lang), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: AppColors.textPrimary, fontSize: 18, fontWeight: FontWeight.w600, height: 1.15, ), ), const SizedBox(height: 2), Text( drama.localizedSynopsis(lang), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: AppColors.textPrimary.withValues(alpha: 0.8), fontSize: 12, ), ), ], ), ), ], ), ), ), ); } }