203 lines
6.8 KiB
Dart
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(0x80000000),
|
|
Color(0x33000000),
|
|
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),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|