Files
ReelShort/lib/pages/following/widgets/continue_watching_banner.dart
T
Vicky 26321e3348
flutter-ci / test (push) Canceled after 0s
feat(cover): 支持横版封面并收紧底栏图标间距
- Drama 新增 coverLandscape,WatchProgress 新增 dramaCoverLandscape
- 继续观看横幅优先使用横版封面,缺省回退竖版
- 底部导航栏图标与文字间距由 5dp 收紧至 1dp
2026-10-29 17:32:41 +08:00

203 lines
6.8 KiB
Dart

// Author: fruit.com.cn
import 'package:flutter/material.dart';
import '../../../core/data/models/watch_record.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart';
/// 追剧页「继续观看」大横幅:取最近一条观看记录,点击续播。
class ContinueWatchingBanner extends StatelessWidget {
const ContinueWatchingBanner({
super.key,
required this.record,
required this.languageCode,
required this.onTap,
});
final WatchRecord record;
final String languageCode;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final episode = record.episodeNumber;
final total =
record.progress.totalEpisodes ?? record.drama?.episodeCount ?? 0;
final title = record.localizedDisplayTitle(languageCode);
final showMeta = episode > 0 && total > 0;
return SizedBox(
height: AppDimens.followingBannerHeight,
child: ClipRRect(
borderRadius: BorderRadius.circular(AppDimens.followingBannerRadius),
child: Stack(
fit: StackFit.expand,
children: [
RemoteImage(
record.displayLandscapeCover,
fit: BoxFit.cover,
showFallbackIcon: true,
fallbackColor: AppColors.contentSurface,
),
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Color(0xCC000000),
Color(0x99000000),
Color(0x00000000),
],
stops: [0, 0.45, 1],
),
),
),
Material(
type: MaterialType.transparency,
child: InkWell(
key: const Key('following-continue-banner'),
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(
AppDimens.followingBannerInnerGap,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (title.isNotEmpty)
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.bannerTitleWarm,
fontSize: AppDimens.followingBannerTitleFont,
fontWeight: FontWeight.w400,
),
),
if (showMeta) ...[
const SizedBox(
height: AppDimens.followingBannerMetaGap,
),
Text(
l10n.continueWatchingSubtitle(episode, total),
maxLines: 1,
style: const TextStyle(
color: Color(0xB3F5EFE4),
fontSize:
AppDimens.followingBannerSubtitleFont,
),
),
],
const SizedBox(
height: AppDimens.followingBannerButtonTopGap,
),
_ContinueButton(label: l10n.continueWatching),
],
),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: _ProgressBar(fraction: record.progress.fraction),
),
],
),
),
),
),
],
),
),
);
}
}
class _ContinueButton extends StatelessWidget {
const _ContinueButton({required this.label});
final String label;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
color: AppColors.brandRed,
borderRadius: BorderRadius.circular(
AppDimens.followingBannerButtonRadius,
),
),
child: SizedBox(
height: AppDimens.followingBannerButtonHeight,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppDimens.followingBannerButtonHorizontalPadding,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.play_arrow_rounded,
size: AppDimens.followingBannerButtonIcon,
color: AppColors.onPrimary,
),
const SizedBox(width: AppDimens.followingBannerButtonGap),
Text(
label,
style: const TextStyle(
color: AppColors.onPrimary,
fontSize: AppDimens.followingBannerButtonFont,
fontWeight: FontWeight.w400,
),
),
],
),
),
),
);
}
}
class _ProgressBar extends StatelessWidget {
const _ProgressBar({required this.fraction});
final double fraction;
@override
Widget build(BuildContext context) {
// SizedBox 给紧约束让 DecoratedBox 轨道撑满整宽;
// 不能用无子 ColoredBox 直接放 Stack(会缩成 0×0 隐形)。
return ClipRRect(
borderRadius: BorderRadius.circular(
AppDimens.followingBannerProgressHeight,
),
child: SizedBox(
height: AppDimens.followingBannerProgressHeight,
width: double.infinity,
child: DecoratedBox(
decoration: const BoxDecoration(color: Color(0x66FFFFFF)),
child: Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: fraction.clamp(0.0, 1.0).toDouble(),
// 无 child 的 ColoredBox 在松约束下会缩成 0×0,高度必须显式占满。
heightFactor: 1.0,
child: const ColoredBox(color: AppColors.brandRed),
),
),
),
),
);
}
}